Repository: osa1/tiny (language: rust)

Issue:
Crash when log_dir: points to an inaccessible location
When I try to run `tiny` on macOS 10.15 (19A512f), it panics with this output:

```
Any mentions to you will be listed here.
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', src/libcore/result.rs:1084:5
mentions                                                                                                                                                                    note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
```

Running with backtrace gives me:

```
Any mentions to you will be listed here.
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', src/libcore/result.rs:1084:5
mentions                                                                                                                                                                    stack backtrace:
                                                                                                                                                                                               0: std::panicking::default_hook::{{closure}}
                           1: std::panicking::rust_panic_with_hook
                                                                     2: std::panicking::continue_panic_fmt
                                                                                                             3: rust_begin_unwind
                                                                                                                                    4: core::panicking::panic_fmt
                                                                                                                                                                    5: core::result::unwrap_failed
                                                                                                                                                                                                     6: tiny::Tiny::run
       7: tiny::main
                       8: std::rt::lang_start::{{closure}}
                                                             9: main
```

I built `tiny` with rustc 1.38.0-nightly (d3f8a0b5d 2019-08-04) from commit 35752a3.


Discussion / hints:
Thanks for reporting. Can you try with a debug build, running tiny with `tiny 2>stderr` ? After the panic the file `stderr` should contain better formatted and more detailed backtrace. Just run it with

```
$ cargo run 2>stderr
```

Thanks!
Sure:

```
$ env RUST_BACKTRACE=1 cargo +nightly run 2>stderr; bat -p stderr
    Finished dev [unoptimized + debuginfo] target(s) in 0.06s
     Running `target/debug/tiny`
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }', src/libcore/result.rs:1084:5
stack backtrace:
   0: backtrace::backtrace::libunwind::trace
             at /Users/vsts/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.34/src/backtrace/libunwind.rs:88
   1: backtrace::backtrace::trace_unsynchronized
             at /Users/vsts/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.34/src/backtrace/mod.rs:66
   2: std::sys_common::backtrace::_print
             at src/libstd/sys_common/backtrace.rs:47
   3: std::sys_common::backtrace::print
             at src/libstd/sys_common/backtrace.rs:36
   4: std::panicking::default_hook::{{closure}}
             at src/libstd/panicking.rs:200
   5: std::panicking::default_hook
             at src/libstd/panicking.rs:214
   6: std::panicking::rust_panic_with_hook
             at src/libstd/panicking.rs:477
   7: std::panicking::continue_panic_fmt
             at src/libstd/panicking.rs:384
   8: rust_begin_unwind
             at src/libstd/panicking.rs:311
   9: std::panicking::begin_panic
  10: std::panicking::begin_panic
  11: core::result::Result<T,E>::unwrap
             at /rustc/d3f8a0b5dfddfe443d9db1f1da18348dbceb0e47/src/libcore/result.rs:852
  12: tiny::logger::Logger::new
             at src/logger.rs:55
  13: tiny::Tiny::run
             at src/lib.rs:176
  14: tiny::run
             at src/lib.rs:105
  15: tiny::main
             at bin/main.rs:4
  16: std::rt::lang_start::{{closure}}
             at /rustc/d3f8a0b5dfddfe443d9db1f1da18348dbceb0e47/src/libstd/rt.rs:64
  17: std::rt::lang_start_internal::{{closure}}
             at src/libstd/rt.rs:49
  18: std::panicking::try::do_call
             at src/libstd/panicking.rs:296
  19: __rust_maybe_catch_panic
             at src/libpanic_unwind/lib.rs:80
  20: std::panicking::try
             at src/libstd/panicking.rs:275
  21: std::panic::catch_unwind
             at src/libstd/panic.rs:394
  22: std::rt::lang_start_internal
             at src/libstd/rt.rs:48
  23: std::rt::lang_start
             at /rustc/d3f8a0b5dfddfe443d9db1f1da18348dbceb0e47/src/libstd/rt.rs:64
  24: tiny::main
```
So it seems like tiny is failing to open the log file. I'd check the log directory in `~/.tinyrc.yml` (see the line starting with `log_dir:`) and make sure it's a valid path. The log file tiny tries to create is `<log_dir>/debug.log` (where `<log_dir>` is the `log_dir` field in your `tinyrc.yml`).

We should do a better job at handling file system errors.
Ah, this is becoming more interesting now.

I checked my `~/.tinyrc.yml` and found that `log_dir:` was pointing to `/Users/soren/.tiny_logs`, which is an absolute path to a directory in the home folder of the *other account* on my machine. This is my fault and due to config file management/syncing between machines/accounts, and explains why the error was a "permission denied" error 13 rather than a "no such file or directory" error 2.

However, it seems the reason I had an absolute path in my config file like this is because, when I change it to `~/.tiny_logs`,

- `tiny` launches without crashing, but
- doesn't create this directory when it runs, and
- doesn't write any logs inside the directory if I create the directory myself before running it.

If I change it to `/Users/snm/.tiny_logs`, which is a correct path inside the home folder of this account, `tiny` both creates the directory and writes logs to it.

The config value `$HOME/.tiny_logs` doesn't work either.

Is there any way support could be added for either the `~/.tiny_logs` or the `$HOME/.tiny_logs` syntax in the config file, please? This would allow me to remove the absolute path from my config file, which would eliminate this crash and allow my config file to be truly machine- and user-independent. I'm happy to file a separate issue for this if you'd like me to.
So you want to support `~` syntax and shell variables. I think this shouldn't be too hard to implement, but I can't tell how off the top of my head. I'm lacking time to work on tiny these days so I'm afraid these will have to be implemented by someone other than me. Would you be willing to implement this yourself? I'd be happy to help if you have any questions.
No worries, I can look into that. Thanks for all your help!

Fix patch:
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 00000000..34055530
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,35 @@
+{
+    // Use IntelliSense to learn about possible attributes.
+    // Hover to view descriptions of existing attributes.
+    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "lldb",
+            "request": "launch",
+            "name": "Debug unit tests in executable 'tiny'",
+            "cargo": {
+                "args": [
+                    "test",
+                    "--no-run",
+                    "--bin=tiny",
+                    "--package=tiny"
+                ],
+                "filter": {
+                    "name": "tiny",
+                    "kind": "bin"
+                }
+            },
+            "args": [],
+            "cwd": "${workspaceFolder}"
+        },
+        {
+            "type": "lldb",
+            "request": "attach",
+            "name": "Attach to executable 'tiny'",
+            "program": "${workspaceFolder}/target/debug/tiny",
+            "pid": "${command:pickProcess}",
+            "stopOnEntry": true,
+        }
+    ]
+}
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..1333ed77
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,1 @@
+TODO
diff --git a/Cargo.toml b/Cargo.toml
index 14428d0a..c96d99cd 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,44 +1,39 @@
 [package]
 name = "tiny"
-version = "0.4.5"
+version = "0.5.0"
 authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
 repository = "https://github.com/osa1/tiny"
 readme = "README.md"
 license = "MIT"
-keywords = ["irc"]
-categories = ["network-programming"]
 description = "An IRC client"
 edition = "2018"
 
-[badges]
-travis-ci = { repository = "osa1/tiny" }
-
-[lib]
-name = "tiny"
-path = "src/lib.rs"
-
-[[bin]]
-name = "tiny"
-path = "bin/main.rs"
+[workspace]
+members = [
+    "libtiny_client",
+    "libtiny_logger",
+    "libtiny_tui",
+    "libtiny_ui",
+    "libtiny_wire",
+    "term_input",
+    "termbox",
+]
 
 [dependencies]
-base64 = "0.6.0"
 dirs = "1.0.2"
-libc = "0.2.22"
-mio = "0.6.9"
-native-tls = "0.2.2"
-net2 = "0.2.23"
-notify-rust = "3"
+env_logger = "0.7"
+futures-util-preview = "0.3.0-alpha.18"
+libtiny_client = { path = "libtiny_client" }
+libtiny_logger = { path = "libtiny_logger" }
+libtiny_tui = { path = "libtiny_tui" }
+libtiny_ui = { path = "libtiny_ui" }
+libtiny_wire = { path = "libtiny_wire" }
+log = "0.4"
 serde = { version = "1.0.8", features = ["derive"] }
 serde_yaml = "0.7.1"
-take_mut = "0.2.0"
-tempfile = "3.0.3"
-term_input = { path = "term_input", version = "0.1.5" }
-termbox_simple = { path = "termbox", version = "0.2.3" }
 time = "0.1"
-
-[dev-dependencies]
-quickcheck = "0.3"
+tokio = { git = "https://github.com/tokio-rs/tokio.git", features = ["timer"] }
+tokio-executor = { git = "https://github.com/tokio-rs/tokio.git" }
 
 [profile.release]
 lto = true
diff --git a/README.md b/README.md
index 538d4bbe..49e50264 100644
--- a/README.md
+++ b/README.md
@@ -198,47 +198,26 @@ Commands start with `/` character.
 
 - `/switch <string>`: Switch to the first tab which has the given string in the name.
 
-- `/ignore`: Ignore `join/quit` messages in a channel. When enabled channel
-  name will have `[i]` added to the end to show that it is enabled. Running
-  this command in a server tab applies it to all channels of that server.
+- `/statusline`: Enable or disable status line on top which gives you info about
+  current settings of a tab.
 
-- `/notify [off|mentions|messages]`: Enable and disable desktop notifications.
-  You can use `/notify` command without any arguments to see the current mode.
-  Running this command in a server tab applies it to all channels of that server.
-
-## Development
-
-tiny is in early stages of development. Some of features that you
-might think are essential may be missing. Please open an issue for
-those features (or even better, a pull request!). Remember to add
-yourself to the [list of contributors](#contributors) if you submit a
-pull request.
+- `/ignore`: Ignore `join/quit` messages in a channel. Running this command in
+  a server tab applies it to all channels of that server. You can check your
+  ignore state in the status line.
 
-Being a Rust n00b, I spent a lot of time trying to convince borrow checker,
-failing at that, and either adding bunch of ugly hacks (e.g. instead a loop that
-modifies single element implementing a loop that finds an index and modifying
-that index after the loop), or using a simpler but less efficient code (e.g.
-cloning things redundantly to avoid borrow checking). Still, I think the
-connection manager (`conn.rs`) and UI parts (`tui` module) are not too bad.
-Mess is mostly in `lib.rs` which implements the main logic (command and message
-handling etc.).
+- `/notify [off|mentions|messages]`: Enable and disable desktop notifications.
+  Running this command in a server tab applies it to all channels of that
+  server. You can check your notify state in the status line.
 
-Any suggestions / code reviews / pull requests are very welcome!
+## Contributing
 
-If you're looking to contribute please see open issues. If you don't know where
-to start, write a comment in the issue you'd like to work on and I'd love to
-help.
+See [`CONTRIBUTING.md`](CONTRIBUTING.md).
 
 ## Contributors
 
-[Ömer Sinan Ağacan](https://github.com/osa1)
-
-[Abin Simon](https://github.com/meain)
-
-[Umur Gedik](https://github.com/umurgdk)
-
-[Chiu Yue Chun](https://github.com/BrianOn99)
-
-[@wammies](https://github.com/wammies)
-
-[Nick Econopouly](https://github.com/wrycode)
+- [Ömer Sinan Ağacan](https://github.com/osa1)
+- [Abin Simon](https://github.com/meain)
+- [Umur Gedik](https://github.com/umurgdk)
+- [Chiu Yue Chun](https://github.com/BrianOn99)
+- [@wammies](https://github.com/wammies)
+- [Nick Econopouly](https://github.com/wrycode)
diff --git a/bin/main.rs b/bin/main.rs
deleted file mode 100644
index 8f2b944d..00000000
--- a/bin/main.rs
+++ /dev/null
@@ -1,5 +0,0 @@
-extern crate tiny;
-
-fn main() {
-    tiny::run();
-}
diff --git a/ev_loop/Cargo.toml b/ev_loop/Cargo.toml
deleted file mode 100644
index 2b6fcc3b..00000000
--- a/ev_loop/Cargo.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[package]
-name = "ev_loop"
-version = "0.1.0"
-authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
-
-[dependencies]
-libc = "0.2.22"
diff --git a/ev_loop/src/lib.rs b/ev_loop/src/lib.rs
deleted file mode 100644
index 606447b3..00000000
--- a/ev_loop/src/lib.rs
+++ /dev/null
@@ -1,287 +0,0 @@
-// TODO:
-//
-// - Better error reporting
-
-extern crate libc;
-
-use std::collections::HashMap;
-use std::collections::HashSet;
-use std::ops::BitOr;
-
-pub struct EvLoop<Ctx> {
-    fds: HashMap<libc::c_int, Handler<Ctx>>,
-}
-
-enum Handler<Ctx> {
-    Timer { cb: Box<FnMut(&mut EvLoopCtrl<Ctx>, &mut Ctx, u64)> },
-    Signal { cb: Box<FnMut(&mut EvLoopCtrl<Ctx>, &mut Ctx)> },
-    Fd { evs: FdEv, cb: Box<FnMut(FdEv, &mut EvLoopCtrl<Ctx>, &mut Ctx)> },
-}
-
-/// File descriptor events. Use bitwise or to combine.
-pub struct FdEv(libc::c_short);
-
-pub const READ_EV: FdEv = FdEv(libc::POLLIN);
-pub const WRITE_EV: FdEv = FdEv(libc::POLLOUT);
-pub const ERR_EV: FdEv = FdEv(libc::POLLERR);
-pub const HUP_EV: FdEv = FdEv(libc::POLLHUP);
-
-impl BitOr for FdEv {
-    type Output = FdEv;
-
-    fn bitor(self, rhs: FdEv) -> FdEv {
-        FdEv(self.0 | rhs.0)
-    }
-}
-
-pub struct TimerRef(libc::c_int);
-pub struct SignalRef(libc::c_int);
-
-extern "C" {
-    fn timerfd_create(
-        clockid: libc::c_int,
-        flags: libc::c_int
-        ) -> libc::c_int;
-
-    fn timerfd_settime(
-        fd: libc::c_int,
-        flags: libc::c_int,
-        new_value: *const itimerspec,
-        old_value: *mut itimerspec
-        ) -> libc::c_int;
-
-    // fn timerfd_gettime(
-    //     fd: libc::c_int,
-    //     curr_value: *mut libc::itimerspec
-    //     ) -> libc::c_int;
-}
-
-// Can't find itimerspec in libc
-#[repr(C)]
-pub struct itimerspec {
-    it_interval: libc::timespec,
-    it_value: libc::timespec,
-}
-
-pub struct EvLoopCtrl<'a, Ctx: 'a> {
-    stop_ref: &'a mut bool,
-    remove_self: &'a mut bool,
-    new_fds: HashMap<libc::c_int, Handler<Ctx>>,
-    removed_fds: HashSet<libc::c_int>,
-}
-
-impl<'a, Ctx> EvLoopCtrl<'a, Ctx> {
-    pub fn stop(&mut self) {
-        *self.stop_ref = true;
-    }
-
-    pub fn remove_self(&mut self) {
-        *self.remove_self = true;
-    }
-
-    pub fn add_fd(&mut self, fd: libc::c_int, evs: FdEv, cb: Box<FnMut(FdEv, &mut EvLoopCtrl<Ctx>, &mut Ctx)>) {
-        self.new_fds.insert(fd, Handler::Fd { evs: evs, cb: cb });
-        self.removed_fds.remove(&fd);
-    }
-
-    pub fn remove_fd(&mut self, fd: libc::c_int) {
-        self.removed_fds.insert(fd);
-        self.new_fds.remove(&fd);
-    }
-}
-
-fn mk_timespec(millis: i64) -> libc::timespec {
-    let secs: i64 = millis / 1000;
-    let nanos: i64 = (millis % 1000) * 1000000;
-    libc::timespec { tv_sec: secs, tv_nsec: nanos }
-}
-
-impl<Ctx> EvLoop<Ctx> {
-    pub fn new() -> EvLoop<Ctx> {
-        EvLoop { fds: HashMap::new() }
-    }
-
-    /// Register a non-blocking socket. Use the same fd for unregister.
-    pub fn add_fd(&mut self, fd: libc::c_int, evs: FdEv, cb: Box<FnMut(FdEv, &mut EvLoopCtrl<Ctx>, &mut Ctx)>) {
-        self.fds.insert(fd, Handler::Fd { evs: evs, cb: cb });
-    }
-
-    pub fn remove_fd(&mut self, fd: libc::c_int) {
-        self.fds.remove(&fd);
-    }
-
-    /// `timeout` and `period` in milliseconds. `timeout` must be non-zero for this to work. If
-    /// `period` is non-zero, timer expires repeatedly after the initial timeout.
-    pub fn add_timer(&mut self, timeout: i64, period: i64, cb: Box<FnMut(&mut EvLoopCtrl<Ctx>, &mut Ctx, u64)>) -> TimerRef {
-        let fd = unsafe { timerfd_create(libc::CLOCK_MONOTONIC, libc::EFD_NONBLOCK) };
-        assert!(fd != -1);
-
-        let timeout_spec = mk_timespec(timeout);
-        let period_spec = mk_timespec(period);
-        let timerspec = itimerspec { it_interval: period_spec, it_value: timeout_spec };
-
-        assert!(unsafe { timerfd_settime(fd, 0, &timerspec, std::ptr::null_mut()) } != -1);
-        self.fds.insert(fd, Handler::Timer { cb: cb });
-        TimerRef(fd)
-    }
-
-    pub fn remove_timer(&mut self, timer_ref: TimerRef) {
-        assert!(unsafe { libc::close(timer_ref.0) } != -1);
-        self.fds.remove(&timer_ref.0);
-    }
-
-    pub fn add_signal(&mut self, sigs: &libc::sigset_t, cb: Box<FnMut(&mut EvLoopCtrl<Ctx>, &mut Ctx)>) -> SignalRef {
-        // Block the signals we handle using signalfd() so they don't cause signal handlers to run
-        assert!(unsafe { libc::sigprocmask(libc::SIG_BLOCK, sigs as *const libc::sigset_t, std::ptr::null_mut()) } != -1);
-        let fd = unsafe { libc::signalfd(-1, sigs, libc::EFD_NONBLOCK) };
-        assert!(fd != -1);
-        self.fds.insert(fd, Handler::Signal { cb: cb });
-        SignalRef(fd)
-    }
-
-    pub fn remove_signal(&mut self, signal_ref: SignalRef) {
-        self.fds.remove(&signal_ref.0);
-    }
-
-    pub fn run(&mut self, mut ctx: Ctx) -> Ctx {
-        let mut stop = false;
-        while !stop {
-            let mut fds: Vec<libc::pollfd> = Vec::with_capacity(self.fds.len());
-
-            for (fd, handler) in self.fds.iter() {
-                match handler {
-                    &Handler::Timer{ .. } | &Handler::Signal{ .. } => {
-                        fds.push(libc::pollfd { fd: *fd, events: libc::POLLIN, revents: 0 });
-                    }
-                    &Handler::Fd{ ref evs, .. } => {
-                        fds.push(libc::pollfd { fd: *fd, events: evs.0, revents: 0 });
-                    }
-                }
-            }
-
-            assert!(unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as u64, -1) } != -1);
-
-            for pollfd in fds {
-                if pollfd.revents != 0 {
-
-                    let mut remove_fd = false;
-                    {
-                        let mut controller = EvLoopCtrl {
-                            stop_ref: &mut stop,
-                            remove_self: &mut remove_fd,
-                            new_fds: HashMap::with_capacity(0),
-                            removed_fds: HashSet::with_capacity(0),
-                        };
-
-                        match self.fds.get_mut(&pollfd.fd).unwrap() {
-                            &mut Handler::Timer{ ref mut cb } => {
-                                let mut ret: u64 = 0;
-                                assert!(unsafe { libc::read(pollfd.fd, &mut ret as *mut u64 as *mut libc::c_void, 8) } != -1);
-                                cb(&mut controller, &mut ctx, ret);
-                            }
-                            &mut Handler::Signal{ ref mut cb } => {
-                                let mut siginfo: libc::signalfd_siginfo = unsafe { std::mem::zeroed() };
-                                assert!(unsafe {
-                                    libc::read(
-                                        pollfd.fd,
-                                        &mut siginfo as *mut libc::signalfd_siginfo as *mut libc::c_void,
-                                        std::mem::size_of::<libc::signalfd_siginfo>())
-                                } != -1);
-                                cb(&mut controller, &mut ctx);
-                            }
-                            &mut Handler::Fd{ ref mut cb, .. } => {
-                                cb(FdEv(pollfd.revents), &mut controller, &mut ctx);
-                            }
-                        }
-
-                        for (fd, h) in controller.new_fds.into_iter() {
-                            self.fds.insert(fd, h);
-                        }
-
-                        for fd in controller.removed_fds.into_iter() {
-                            self.fds.remove(&fd);
-                        }
-                    }
-
-                    if remove_fd {
-                        self.fds.remove(&pollfd.fd);
-                    }
-                }
-            }
-        }
-        ctx
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::cell::RefCell;
-    use std::rc::Rc;
-
-    #[test]
-    fn it_works() {
-        let count: Rc<RefCell<i32>> = Rc::new(RefCell::new(0));
-
-        let mut ev_loop = EvLoop::new();
-        {
-            let count_clone = count.clone();
-            ev_loop.add_timer(100, 100, Box::new(move |ctrl, _, _| {
-                *count_clone.borrow_mut() += 1;
-                println!("bump");
-                if *(*count_clone).borrow() == 10 {
-                    ctrl.stop();
-                }
-            }));
-        }
-        ev_loop.run(());
-
-        assert_eq!(*(*count).borrow(), 10);
-    }
-
-    #[test]
-    fn it_works_2() {
-        struct Ctx {
-            cb1: u64,
-            cb2: u64,
-            cb3: bool,
-        }
-
-        let mut ev_loop: EvLoop<Ctx> = EvLoop::new();
-
-        ev_loop.add_timer(100, 100, Box::new(move |ctrl, ctx, amt| {
-            assert!(ctx.cb1 == 0);
-            ctx.cb1 = amt;
-            ctrl.remove_self();
-        }));
-
-        ev_loop.add_timer(100, 100, Box::new(move |ctrl, ctx, amt| {
-            assert!(ctx.cb2 == 0);
-            ctx.cb2 = amt;
-            ctrl.remove_self();
-        }));
-
-        {
-            let fd = unsafe { timerfd_create(libc::CLOCK_MONOTONIC, libc::EFD_NONBLOCK) };
-            assert!(fd != -1);
-
-            let timeout_spec = mk_timespec(300);
-            let period_spec = mk_timespec(300);
-            let timerspec = itimerspec { it_interval: period_spec, it_value: timeout_spec };
-
-            assert!(unsafe { timerfd_settime(fd, 0, &timerspec, std::ptr::null_mut()) } != -1);
-            ev_loop.add_fd(fd, READ_EV, Box::new(move |_, ctrl, ctx| {
-                assert!(ctx.cb3 == false);
-                ctx.cb3 = true;
-                ctrl.remove_self();
-                ctrl.stop();
-            }));
-        }
-
-        let ctx = ev_loop.run(Ctx { cb1: 0, cb2: 0, cb3: false });
-
-        assert_eq!(ctx.cb1, 1);
-        assert_eq!(ctx.cb2, 1);
-        assert!(ctx.cb3);
-    }
-}
diff --git a/libtiny_client/Cargo.toml b/libtiny_client/Cargo.toml
new file mode 100644
index 00000000..b850c096
--- /dev/null
+++ b/libtiny_client/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "libtiny_client"
+version = "0.1.0"
+authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
+description = "An IRC client library, mainly to be used in tiny"
+edition = "2018"
+
+[dependencies]
+base64 = "0.6.0"
+env_logger = "0.7"
+futures-preview = { version = "0.3.0-alpha.18",  features = ["async-await", "nightly"] }
+futures-util-preview = "0.3.0-alpha.18"
+libtiny_logger = { path = "../libtiny_logger" }
+libtiny_wire = { path = "../libtiny_wire" }
+log = "0.4"
+native-tls = "0.2"
+tokio = { git = "https://github.com/tokio-rs/tokio.git", features = ["timer"] }
+tokio-executor = { git = "https://github.com/tokio-rs/tokio.git" }
+tokio-tls = { git = "https://github.com/tokio-rs/tokio.git" }
diff --git a/libtiny_client/examples/ex1.rs b/libtiny_client/examples/ex1.rs
new file mode 100644
index 00000000..c3ce257d
--- /dev/null
+++ b/libtiny_client/examples/ex1.rs
@@ -0,0 +1,62 @@
+use futures_util::stream::StreamExt;
+use std::time::Duration;
+
+use libtiny::{Client, ServerInfo};
+
+fn main() {
+    let mut executor = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+    let server_info = ServerInfo {
+        addr: "chat.freenode.net".to_string(),
+        port: 6667,
+        tls: false,
+        pass: None,
+        hostname: "omer".to_string(),
+        realname: "omer".to_string(),
+        nicks: vec!["osa1".to_string()],
+        auto_join: vec![],
+        nickserv_ident: None,
+        sasl_auth: None,
+    };
+
+    executor.spawn(async {
+        let (_client, mut rcv_ev) = Client::new(server_info, None);
+        println!("client created");
+        while let Some(ev) = rcv_ev.next().await {
+            println!("ev: {:?}", ev);
+        }
+    });
+
+    let server_info = ServerInfo {
+        addr: "chat.freenode.net".to_string(),
+        port: 6667,
+        tls: false,
+        pass: None,
+        hostname: "omer".to_string(),
+        realname: "omer".to_string(),
+        nicks: vec!["osa1s_irc_bot".to_string()],
+        auto_join: vec!["#justtesting".to_string()],
+        nickserv_ident: None,
+        sasl_auth: None,
+    };
+
+    executor.spawn(async {
+        println!("Sleeping for 3 seconds before the second connection");
+        tokio::timer::delay_for(Duration::from_secs(3)).await;
+        let (mut client, mut rcv_ev) = Client::new(server_info, None);
+
+        println!("client created, spawning incoming msg handler task");
+
+        tokio::spawn(async move {
+            while let Some(ev) = rcv_ev.next().await {
+                println!("ev: {:?}", ev);
+            }
+        });
+
+        println!("sleeping for 5 seconds before joining #justtesting");
+        tokio::timer::delay_for(Duration::from_secs(5)).await;
+        client.join(&["#justtesting"]);
+    });
+
+    executor.run().unwrap(); // unwraps RunError
+}
diff --git a/libtiny_client/src/lib.rs b/libtiny_client/src/lib.rs
new file mode 100644
index 00000000..4c0a4d88
--- /dev/null
+++ b/libtiny_client/src/lib.rs
@@ -0,0 +1,644 @@
+#![recursion_limit = "512"]
+#![feature(test)]
+#![allow(clippy::unneeded_field_pattern)]
+#![allow(clippy::cognitive_complexity)]
+
+mod pinger;
+mod state;
+mod stream;
+mod utils;
+
+pub use libtiny_wire as wire;
+
+use pinger::Pinger;
+use state::State;
+use stream::{Stream, StreamError};
+
+use futures::future::FutureExt;
+use futures::stream::StreamExt;
+use futures::{pin_mut, select};
+use futures_util::stream::Fuse;
+use std::net::{SocketAddr, ToSocketAddrs};
+use std::time::Duration;
+use tokio::io::{AsyncReadExt, AsyncWriteExt};
+use tokio::runtime::current_thread::Runtime;
+use tokio::sync::mpsc;
+
+#[macro_use]
+extern crate log;
+
+//
+// Public API
+//
+
+/// `Client` tries to reconnect on error after this many seconds.
+pub const RECONNECT_SECS: u64 = 30;
+
+#[derive(Debug, Clone)]
+pub struct ServerInfo {
+    /// Server address
+    pub addr: String,
+
+    /// Server port
+    pub port: u16,
+
+    /// Use TLS?
+    pub tls: bool,
+
+    /// Server password.
+    pub pass: Option<String>,
+
+    pub hostname: String,
+
+    pub realname: String,
+
+    /// Nicks to select when logging in.
+    pub nicks: Vec<String>,
+
+    /// Channels to automatically join
+    pub auto_join: Vec<String>,
+
+    /// Nickserv password. Sent to NickServ on connecting to the server and nick change, before
+    /// join commands.
+    pub nickserv_ident: Option<String>,
+
+    /// SASL authentication credentials,
+    pub sasl_auth: Option<SASLAuth>,
+}
+
+/// SASL authentication credentials
+#[derive(Debug, Clone)]
+pub struct SASLAuth {
+    pub username: String,
+    pub password: String,
+}
+
+/// IRC client events. Returned by `Client` to the users via a channel.
+#[derive(Debug)]
+pub enum Event {
+    /// Client trying to connect
+    Connecting,
+    /// TCP connection established *and* the introduction sequence with the IRC server started.
+    Connected,
+    /// Disconnected from the server. Usually sent right after an `Event::IoErr`.
+    Disconnected,
+    /// An IO error happened.
+    IoErr(std::io::Error),
+    /// A TLS error happened
+    TlsErr(native_tls::Error),
+    /// Client couldn't resolve host address.
+    CantResolveAddr,
+    /// Nick changed.
+    NickChange(String),
+    /// A message from the server
+    Msg(wire::Msg),
+
+    /// This is to signal the task that listens for events to stop.
+    // TODO: Maybe try something like making Client non-Clone and sharing Weaks with tasks
+    Closed,
+}
+
+impl From<StreamError> for Event {
+    fn from(err: StreamError) -> Event {
+        match err {
+            StreamError::TlsError(tls_err) => Event::TlsErr(tls_err),
+            StreamError::IoError(io_err) => Event::IoErr(io_err),
+        }
+    }
+}
+
+/// IRC client.
+#[derive(Clone)]
+pub struct Client {
+    /// Channel to the send commands to the main loop. Usually just for sending messages to the
+    /// server.
+    msg_chan: mpsc::Sender<Cmd>,
+
+    // TODO: This is mostly here to make switching from the old `conn.rs` easier; it may be
+    // possible to remove this and maybe have a unique usize in each client as id.
+    serv_name: String,
+
+    /// Reference to the state, to be able to provide methods like `get_nick` and
+    /// `is_nick_accepted`.
+    state: State,
+    // We can't have a channel to the sender task directly here, because when the sender task
+    // returns we lose the receiving end of the channel and there's no way to avoid this except
+    // with annoying hacks like wrapping it with an `Arc<Mutex<..>>` or something.
+    snd_ev: mpsc::Sender<Event>,
+}
+
+impl Client {
+    /// Create a new client. Spawns two `tokio` tasks on the given `runtime`. If not given, tasks
+    /// are created on the default executor using `tokio::spawn`.
+    pub fn new(
+        server_info: ServerInfo,
+        runtime: Option<&mut Runtime>,
+    ) -> (Client, mpsc::Receiver<Event>) {
+        connect(server_info, runtime)
+    }
+
+    /// Reconnect to the server, possibly using a new port.
+    pub fn reconnect(&mut self, port: Option<u16>) {
+        debug!("reconnect cmd received, port: {:?}", port);
+        self.msg_chan.try_send(Cmd::Reconnect(port)).unwrap()
+    }
+
+    /// Get host name of this connection.
+    pub fn get_serv_name(&self) -> &str {
+        &self.serv_name
+    }
+
+    /// Get current nick. Not that this returns the nick we're currently trying when the nick is
+    /// not yet accepted. See `is_nick_accepted`.
+    // FIXME: This allocates a String
+    pub fn get_nick(&self) -> String {
+        self.state.get_nick()
+    }
+
+    /// Is current nick accepted by the server?
+    // TODO: Do we really need this?
+    pub fn is_nick_accepted(&self) -> bool {
+        self.state.is_nick_accepted()
+    }
+
+    /// Send a message directly to the server. "\r\n" suffix is added by this method.
+    pub fn raw_msg(&mut self, msg: &str) {
+        self.msg_chan
+            .try_send(Cmd::Msg(format!("{}\r\n", msg)))
+            .unwrap();
+    }
+
+    /// Split a privmsg to multiple messages so that each message is, when the hostname and nick
+    /// prefix added by the server, fits in one IRC message.
+    ///
+    /// `extra_len`: Size (in bytes) for a prefix/suffix etc. that'll be added to each line.
+    pub fn split_privmsg<'a>(
+        &self,
+        extra_len: usize,
+        msg: &'a str,
+    ) -> impl Iterator<Item = &'a str> {
+        // Max msg len calculation adapted from hexchat
+        // (src/common/outbound.c:split_up_text)
+        let mut max = 512; // RFC 2812
+        max -= 3; // :, !, @
+        max -= 13; // " PRIVMSG ", " ", :, \r, \n
+        max -= self.get_nick().len();
+        max -= extra_len;
+        match self.state.get_usermask() {
+            None => {
+                max -= 9; // max username
+                max -= 64; // max possible hostname (63) + '@'
+                           // NOTE(osa): I think hexchat has an error here, it
+                           // uses 65
+            }
+            Some(ref usermask) => {
+                max -= usermask.len();
+            }
+        }
+
+        assert!(max > 0);
+
+        utils::split_iterator(msg, max)
+    }
+
+    /// Send a privmsg. Note that this method does not split long messages into smaller messages;
+    /// use `split_privmsg` for that.
+    pub fn privmsg(&mut self, target: &str, msg: &str, is_action: bool) {
+        let wire_fn = if is_action {
+            wire::action
+        } else {
+            wire::privmsg
+        };
+        self.msg_chan
+            .try_send(Cmd::Msg(wire_fn(target, msg)))
+            .unwrap();
+    }
+
+    /// Join the given list of channels.
+    pub fn join(&mut self, chans: &[&str]) {
+        self.msg_chan
+            .try_send(Cmd::Msg(wire::join(&chans)))
+            .unwrap()
+    }
+
+    /// Leave a channel.
+    pub fn part(&mut self, chan: &str) {
+        self.msg_chan.try_send(Cmd::Msg(wire::part(chan))).unwrap();
+    }
+
+    /// Set away status. `None` means not away.
+    pub fn away(&mut self, msg: Option<&str>) {
+        self.state.set_away(msg);
+        self.msg_chan.try_send(Cmd::Msg(wire::away(msg))).unwrap()
+    }
+
+    /// Change nick. This may fail (ERR_NICKNAMEINUSE) so wait for confirmation (a NICK message
+    /// back from the server, with the old nick as prefix).
+    pub fn nick(&mut self, new_nick: &str) {
+        self.msg_chan
+            .try_send(Cmd::Msg(wire::nick(new_nick)))
+            .unwrap()
+    }
+
+    /// Send a QUIT message to the server, with optional "reason". This stops the client; so the
+    /// sender end of the `Cmd` channel and the receiver end of the IRC message channel (for
+    /// outgoing messages) will be dropped.
+    pub fn quit(&mut self, reason: Option<String>) {
+        debug!("quit cmd received");
+        self.msg_chan.try_send(Cmd::Quit(reason)).unwrap();
+        self.snd_ev.try_send(Event::Closed).unwrap();
+    }
+
+    /// Get all nicks in a channel.
+    pub fn get_chan_nicks(&self, chan: &str) -> Vec<String> {
+        self.state.get_chan_nicks(chan)
+    }
+}
+
+//
+// End of public API
+//
+
+#[derive(Debug)]
+enum Cmd {
+    /// Send this IRC message to the server. Note that this needs to be a valid IRC message
+    /// (including the trailing "\r\n").
+    Msg(String),
+    /// Reconnect to the server, possibly using a new port.
+    Reconnect(Option<u16>),
+    /// Close the connection. This sends a QUIT message to the server (with optional "reason") and
+    /// then all tasks return.
+    Quit(Option<String>),
+}
+
+fn connect(
+    server_info: ServerInfo,
+    runtime: Option<&mut Runtime>,
+) -> (Client, mpsc::Receiver<Event>) {
+    let serv_name = server_info.addr.clone();
+
+    //
+    // Create communication channels
+    //
+
+    // Channel for returning IRC events to user.
+    let (snd_ev, rcv_ev) = mpsc::channel::<Event>(100);
+    let snd_ev_clone = snd_ev.clone(); // for Client
+
+    // Channel for commands from user.
+    let (snd_cmd, rcv_cmd) = mpsc::channel::<Cmd>(100);
+
+    //
+    // Create the main loop task
+    //
+
+    let irc_state = State::new(server_info.clone());
+    let irc_state_clone = irc_state.clone();
+
+    let task = main_loop(server_info, irc_state_clone, snd_ev, rcv_cmd);
+
+    match runtime {
+        Some(runtime) => {
+            runtime.spawn(task);
+        }
+        None => {
+            tokio::runtime::current_thread::spawn(task);
+        }
+    }
+
+    (
+        Client {
+            msg_chan: snd_cmd,
+            serv_name,
+            state: irc_state,
+            snd_ev: snd_ev_clone,
+        },
+        rcv_ev,
+    )
+}
+
+use TaskResult::*;
+
+async fn main_loop(
+    server_info: ServerInfo,
+    irc_state: State,
+    mut snd_ev: mpsc::Sender<Event>,
+    rcv_cmd: mpsc::Receiver<Cmd>,
+) {
+    let mut rcv_cmd = rcv_cmd.fuse();
+
+    // We allow changing ports when reconnecting, so `mut`
+    let mut port = server_info.port;
+
+    // Whether to wait before trying to (re)connect
+    let mut wait = false;
+
+    // Main loop just tries to (re)connect
+    'connect: loop {
+        if wait {
+            match wait_(&mut rcv_cmd).await {
+                Done(()) => {}
+                TryWithPort(new_port) => {
+                    port = new_port;
+                    wait = false;
+                    continue;
+                }
+                TryReconnect => {
+                    wait = false;
+                    continue;
+                }
+                TryAfterDelay => {
+                    panic!("wait() returned TryAfterDelay");
+                }
+                Return => {
+                    return;
+                }
+            }
+        }
+
+        // Channel for the sender task. Messages are complete IRC messages (including the
+        // trailing "\r\n") and the task directly sends them to the server.
+        let (mut snd_msg, mut rcv_msg) = mpsc::channel::<String>(100);
+
+        snd_ev.try_send(Event::Connecting).unwrap();
+
+        //
+        // Resolve IP address
+        //
+
+        let serv_name = server_info.addr.clone();
+
+        debug!("Resolving address");
+
+        let serv_name_clone = serv_name.clone();
+
+        let mut addr_iter =
+            match resolve_addr(serv_name_clone, port, &mut rcv_cmd, &mut snd_ev).await {
+                Done(addr_iter) => {
+                    debug!("resolve_addr: done");
+                    addr_iter
+                }
+                TryWithPort(new_port) => {
+                    debug!("resolve_addr: try new port");
+                    port = new_port;
+                    wait = false;
+                    continue;
+                }
+                TryReconnect => {
+                    debug!("resolve_addr: try again");
+                    wait = false;
+                    continue;
+                }
+                TryAfterDelay => {
+                    debug!("resolve_addr: try after delay");
+                    wait = true;
+                    continue;
+                }
+                Return => {
+                    debug!("resolve_addr: return");
+                    return;
+                }
+            };
+
+        debug!("Address resolved");
+
+        let addr = match addr_iter.next() {
+            None => {
+                snd_ev.try_send(Event::CantResolveAddr).unwrap();
+                break;
+            }
+            Some(addr) => addr,
+        };
+
+        //
+        // Establish TCP connection to the server
+        //
+
+        debug!("Establishing connection ...");
+
+        // TODO: This step can block too, but hopefully not too much?
+        let mb_stream = if server_info.tls {
+            Stream::new_tls(addr, &serv_name).await
+        } else {
+            Stream::new_tcp(addr).await
+        };
+
+        let stream = match mb_stream {
+            Ok(stream) => stream,
+            Err(err) => {
+                snd_ev.try_send(Event::from(err)).unwrap();
+                snd_ev.try_send(Event::Disconnected).unwrap();
+                wait = true;
+                continue;
+            }
+        };
+
+        let (mut read_half, mut write_half) = tokio::io::split(stream);
+
+        debug!("Done");
+
+        //
+        // Do the business
+        //
+
+        // Reset the connection state
+        irc_state.reset();
+        // Introduce self
+        if server_info.sasl_auth.is_some() {
+            // Will introduce self after getting a response to this LS command.
+            // This is to avoid getting stuck during nick registration. See the
+            // discussion in #91.
+            snd_msg.try_send(wire::cap_ls()).unwrap();
+        } else {
+            irc_state.introduce(&mut snd_msg);
+        }
+
+        // Spawn a task for outgoing messages.
+        let mut snd_ev_clone = snd_ev.clone();
+        tokio::runtime::current_thread::spawn(async move {
+            while let Some(msg) = rcv_msg.next().await {
+                if let Err(io_err) = write_half.write_all(msg.as_str().as_bytes()).await {
+                    debug!("IO error when writing: {:?}", io_err);
+                    snd_ev_clone.try_send(Event::IoErr(io_err)).unwrap();
+                    return;
+                }
+            }
+        });
+
+        // Spawn pinger task
+        let (mut pinger, rcv_ping_evs) = Pinger::new();
+        let mut rcv_ping_evs_fused = rcv_ping_evs.fuse();
+
+        let mut parse_buf: Vec<u8> = Vec::with_capacity(1024);
+
+        loop {
+            let mut read_buf: [u8; 1024] = [0; 1024];
+
+            select! {
+                cmd = rcv_cmd.next() => {
+                    match cmd {
+                        None => {
+                            debug!("main loop: command channel terminated from the other end");
+                            // That's OK, rcv_cmd will never be ready again
+                        }
+                        Some(Cmd::Msg(irc_msg)) => {
+                            snd_msg.try_send(irc_msg).unwrap();
+                        }
+                        Some(Cmd::Reconnect(mb_port)) => {
+                            if let Some(new_port) = mb_port {
+                                port = new_port;
+                            }
+                            wait = false;
+                            continue 'connect;
+                        }
+                        Some(Cmd::Quit(reason)) => {
+                            snd_msg.try_send(wire::quit(reason)).unwrap();
+                            // This drops the sender end of the channel that the sender task
+                            // uses, which in turn causes the sender task to return. Somewhat
+                            // hacky?
+                            return;
+                        }
+                    }
+                }
+                bytes = read_half.read(&mut read_buf).fuse() => {
+                    match bytes {
+                        Err(io_err) => {
+                            debug!("main loop: error when reading from socket: {:?}", io_err);
+                            snd_ev.try_send(Event::IoErr(io_err)).unwrap();
+                            snd_ev.try_send(Event::Disconnected).unwrap();
+                            wait = true;
+                            continue 'connect;
+                        }
+                        Ok(bytes) => {
+                            parse_buf.extend_from_slice(&read_buf[0..bytes]);
+                            while let Some(mut msg) = wire::parse_irc_msg(&mut parse_buf) {
+                                debug!("parsed msg: {:?}", msg);
+                                pinger.reset();
+                                irc_state.update(&mut msg, &mut snd_ev, &mut snd_msg);
+                                snd_ev.try_send(Event::Msg(msg)).unwrap();
+                            }
+                        }
+                    }
+                }
+                ping_ev = rcv_ping_evs_fused.next() => {
+                    match ping_ev {
+                        None => {
+                            debug!("Ping thread terminated unexpectedly???");
+                        }
+                        Some(pinger::Event::SendPing) => {
+                            irc_state.send_ping(&mut snd_msg);
+                        }
+                        Some(pinger::Event::Disconnect) => {
+                            // TODO: indicate that this is a ping timeout
+                            snd_ev.try_send(Event::Disconnected).unwrap();
+                            // TODO: hopefully dropping the pinger rcv end is enough to stop it?
+                            wait = true;
+                            continue 'connect;
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+enum TaskResult<A> {
+    Done(A),
+    TryWithPort(u16),
+    TryReconnect,
+    TryAfterDelay,
+    Return,
+}
+
+async fn wait_(rcv_cmd: &mut Fuse<mpsc::Receiver<Cmd>>) -> TaskResult<()> {
+    // Weird code because of a bug in select!?
+    let delay = async {
+        tokio::timer::delay_for(Duration::from_secs(60)).await;
+    }
+        .fuse();
+    pin_mut!(delay);
+
+    loop {
+        select! {
+            () = delay => {
+                return Done(());
+            }
+            cmd = rcv_cmd.next() => {
+                // FIXME: This whole block is duplicated below, but it's hard to reuse because it
+                // usese `continue` and `return`.
+                match cmd {
+                    None => {
+                        // Channel closed, return from the main loop
+                        return Return;
+                    }
+                    Some(Cmd::Msg(_)) => {
+                        continue;
+                    }
+                    Some(Cmd::Reconnect(mb_port)) => {
+                        match mb_port {
+                            None => {
+                                return TryReconnect;
+                            }
+                            Some(port) => {
+                                return TryWithPort(port);
+                            }
+                        }
+                    }
+                    Some(Cmd::Quit(_)) => {
+                        return Return;
+                    }
+                }
+            }
+        }
+    }
+}
+
+async fn resolve_addr(
+    serv_name: String,
+    port: u16,
+    rcv_cmd: &mut Fuse<mpsc::Receiver<Cmd>>,
+    snd_ev: &mut mpsc::Sender<Event>,
+) -> TaskResult<std::vec::IntoIter<SocketAddr>> {
+    let mut addr_iter_task =
+        tokio_executor::blocking::run(move || (serv_name.as_str(), port).to_socket_addrs()).fuse();
+
+    loop {
+        select! {
+            addr_iter = addr_iter_task => {
+                match addr_iter {
+                    Err(io_err) => {
+                        snd_ev.try_send(Event::IoErr(io_err)).unwrap();
+                        return TryAfterDelay;
+                    }
+                    Ok(addr_iter) => {
+                        return Done(addr_iter);
+                    }
+                }
+            }
+            cmd = rcv_cmd.next() => {
+                match cmd {
+                    None => {
+                        // Channel closed, return from the main loop
+                        return Return;
+                    }
+                    Some(Cmd::Msg(_)) => {
+                        continue;
+                    }
+                    Some(Cmd::Reconnect(mb_port)) => {
+                        match mb_port {
+                            None => {
+                                return TryReconnect;
+                            }
+                            Some(port) => {
+                                return TryWithPort(port);
+                            }
+                        }
+                    }
+                    Some(Cmd::Quit(_)) => {
+                        return Return;
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/libtiny_client/src/pinger.rs b/libtiny_client/src/pinger.rs
new file mode 100644
index 00000000..6cf649ba
--- /dev/null
+++ b/libtiny_client/src/pinger.rs
@@ -0,0 +1,81 @@
+//! Implements two-state "pinger" task that drives sending pings to the server to check liveness of
+//! the connection.
+
+use futures::FutureExt;
+use futures::{pin_mut, select, stream::StreamExt};
+use std::time::Duration;
+use tokio::sync::mpsc;
+use tokio::timer::delay_for;
+
+pub(crate) struct Pinger {
+    snd_rst: mpsc::Sender<()>,
+}
+
+#[derive(Debug)]
+pub(crate) enum Event {
+    SendPing,
+    Disconnect,
+}
+
+enum PingerState {
+    /// Signal a "ping" on timeout. State moves to `ExpectPong`.
+    SendPing,
+    /// Signal a "disconnect" on timeout.
+    ExpectPong,
+}
+
+async fn pinger_task(rcv_rst: mpsc::Receiver<()>, mut snd_ev: mpsc::Sender<Event>) {
+    let mut rcv_rst_fused = rcv_rst.fuse();
+    let mut state = PingerState::SendPing;
+    loop {
+        // NOTE: The code about does not work:
+        // let mut delay = delay_for(Duration::from_secs(30));
+        // Instead I need this weird code below. Not sure if this is a bug or not.
+        let delay = async {
+            delay_for(Duration::from_secs(60)).await;
+        }
+            .fuse();
+        pin_mut!(delay);
+
+        select! {
+            () = delay => {
+                match state {
+                    PingerState::SendPing => {
+                        state = PingerState::ExpectPong;
+                        snd_ev.try_send(Event::SendPing).unwrap();
+                    }
+                    PingerState::ExpectPong => {
+                        snd_ev.try_send(Event::Disconnect).unwrap();
+                        return;
+                    }
+                }
+            }
+            cmd = rcv_rst_fused.next() => {
+                match cmd {
+                    None => {
+                        return;
+                    }
+                    Some(()) => {
+                        state = PingerState::SendPing;
+                    }
+                }
+            }
+        }
+    }
+}
+
+impl Pinger {
+    pub(crate) fn new() -> (Pinger, mpsc::Receiver<Event>) {
+        let (snd_ev, rcv_ev) = mpsc::channel(1);
+        // No need for sending another "reset" when there's already one waiting to be processed
+        let (snd_rst, rcv_rst) = mpsc::channel(1);
+        tokio::runtime::current_thread::spawn(pinger_task(rcv_rst, snd_ev));
+        (Pinger { snd_rst }, rcv_ev)
+    }
+
+    pub(crate) fn reset(&mut self) {
+        // Ignore errors: no need to send another "reset" when there's already one waiting to be
+        // processed
+        let _ = self.snd_rst.try_send(());
+    }
+}
diff --git a/libtiny_client/src/state.rs b/libtiny_client/src/state.rs
new file mode 100644
index 00000000..76035dcf
--- /dev/null
+++ b/libtiny_client/src/state.rs
@@ -0,0 +1,536 @@
+#![allow(clippy::zero_prefixed_literal)]
+
+use crate::utils;
+use crate::{Event, ServerInfo};
+use libtiny_wire as wire;
+use libtiny_wire::{find_byte, Msg, Pfx};
+
+use std::cell::RefCell;
+use std::collections::HashSet;
+use std::rc::Rc;
+use tokio::sync::mpsc::Sender;
+
+#[derive(Clone)]
+pub struct State {
+    inner: Rc<RefCell<StateInner>>,
+}
+
+impl State {
+    pub(crate) fn new(server_info: ServerInfo) -> State {
+        State {
+            inner: Rc::new(RefCell::new(StateInner::new(server_info))),
+        }
+    }
+
+    pub(crate) fn reset(&self) {
+        self.inner.borrow_mut().reset()
+    }
+
+    pub(crate) fn send_ping(&self, snd_irc_msg: &mut Sender<String>) {
+        self.inner.borrow_mut().send_ping(snd_irc_msg)
+    }
+
+    pub(crate) fn update(
+        &self,
+        msg: &mut Msg,
+        snd_ev: &mut Sender<Event>,
+        snd_irc_msg: &mut Sender<String>,
+    ) {
+        self.inner.borrow_mut().update(msg, snd_ev, snd_irc_msg);
+    }
+
+    pub(crate) fn introduce(&self, snd_irc_msg: &mut Sender<String>) {
+        self.inner.borrow_mut().introduce(snd_irc_msg)
+    }
+
+    // FIXME: This allocates a new String
+    pub(crate) fn get_nick(&self) -> String {
+        self.inner.borrow().current_nick.clone()
+    }
+
+    // FIXME: Maybe use RwLock instead of Mutex
+    pub(crate) fn is_nick_accepted(&self) -> bool {
+        self.inner.borrow().nick_accepted
+    }
+
+    pub(crate) fn get_usermask(&self) -> Option<String> {
+        self.inner.borrow().usermask.clone()
+    }
+
+    pub(crate) fn set_away(&self, msg: Option<&str>) {
+        self.inner.borrow_mut().away_status = msg.map(str::to_owned);
+    }
+
+    pub(crate) fn get_chan_nicks(&self, chan: &str) -> Vec<String> {
+        self.inner.borrow().get_chan_nicks(chan)
+    }
+}
+
+struct StateInner {
+    /// Nicks to try, in this order.
+    nicks: Vec<String>,
+
+    /// NickServ passowrd.
+    nickserv_ident: Option<String>,
+
+    /// An index to `nicks`. When out of range we add `current_nick_idx - nicks.length()`
+    /// underscores to the last nick in `nicks`
+    current_nick_idx: usize,
+
+    /// A cache of current nick, to avoid allocating new nicks when inventing new nicks with
+    /// underscores.
+    current_nick: String,
+
+    /// Currently joined channels. Every channel we join will be added here to be able to re-join
+    /// automatically on reconnect and channels we leave will be removed.
+    ///
+    /// This would be a `HashMap<String, ..>` but we want to join channels in the order the user
+    /// specified, so using a `Vec`.
+    ///
+    /// TODO: I'm not sure if this is necessary. Why not just create channel tabs in the specified
+    /// order, in TUI?
+    chans: Vec<(String, HashSet<String>)>,
+
+    /// Away reason if away mode is on. `None` otherwise. TODO: I don't think the message is used?
+    away_status: Option<String>,
+
+    /// servername to be used in PING messages. Read from 002 RPL_YOURHOST. `None` until 002.
+    servername: Option<String>,
+
+    /// Our usermask given by the server. Currently only parsed after a JOIN, reply 396.
+    ///
+    /// Note that RPL_USERHOST (302) does not take cloaks into account, so we don't parse USERHOST
+    /// responses to set this field.
+    usermask: Option<String>,
+
+    /// Do we have a nick yet? Try another nick on ERR_NICKNAMEINUSE (433) until we've got a nick.
+    nick_accepted: bool,
+
+    /// Server information
+    server_info: ServerInfo,
+}
+
+impl StateInner {
+    fn new(server_info: ServerInfo) -> StateInner {
+        let current_nick = server_info.nicks[0].to_owned();
+        let chans = server_info
+            .auto_join
+            .iter()
+            .map(|s| (s.clone(), HashSet::new()))
+            .collect();
+        StateInner {
+            nicks: server_info.nicks.clone(),
+            nickserv_ident: server_info.nickserv_ident.clone(),
+            current_nick_idx: 0,
+            current_nick,
+            chans,
+            away_status: None,
+            servername: None,
+            usermask: None,
+            nick_accepted: false,
+            server_info,
+        }
+    }
+
+    fn reset(&mut self) {
+        self.nicks = self.server_info.nicks.clone();
+        self.current_nick_idx = 0;
+        self.current_nick = self.nicks[0].clone();
+        // Only reset the values here; the key set will be used to join channels
+        for (_, chan) in &mut self.chans {
+            chan.clear();
+        }
+        self.servername = None;
+        self.usermask = None;
+    }
+
+    fn send_ping(&mut self, snd_irc_msg: &mut Sender<String>) {
+        if let Some(ref servername) = self.servername {
+            snd_irc_msg.try_send(wire::ping(servername)).unwrap();
+        }
+    }
+
+    fn introduce(&mut self, snd_irc_msg: &mut Sender<String>) {
+        if let Some(ref pass) = self.server_info.pass {
+            snd_irc_msg.try_send(wire::pass(pass)).unwrap();
+        }
+        snd_irc_msg
+            .try_send(wire::nick(&self.current_nick))
+            .unwrap();
+        snd_irc_msg
+            .try_send(wire::user(
+                &self.server_info.hostname,
+                &self.server_info.realname,
+            ))
+            .unwrap();
+    }
+
+    fn get_next_nick(&mut self) -> &str {
+        self.current_nick_idx += 1;
+        // debug!("current_nick_idx: {}", self.current_nick_idx);
+        if self.current_nick_idx >= self.nicks.len() {
+            let n_underscores = self.current_nick_idx - self.nicks.len() + 1;
+            let mut new_nick = self.nicks.last().unwrap().to_string();
+            for _ in 0..n_underscores {
+                new_nick.push('_');
+            }
+            self.current_nick = new_nick;
+        } else {
+            self.current_nick = self.nicks[self.current_nick_idx].clone();
+        }
+        &self.current_nick
+    }
+
+    fn update(
+        &mut self,
+        msg: &mut Msg,
+        snd_ev: &mut Sender<Event>,
+        snd_irc_msg: &mut Sender<String>,
+    ) {
+        let Msg {
+            ref pfx,
+            ref mut cmd,
+        } = msg;
+
+        use wire::Cmd::*;
+        match cmd {
+            PING { server } => {
+                snd_irc_msg.try_send(wire::pong(server)).unwrap();
+            }
+
+            //
+            // Setting usermask using JOIN, RPL_USERHOST and 396 (?)
+            // Also initialize the channel state on JOIN
+            //
+            JOIN { chan } => {
+                if let Some(Pfx::User { nick, user }) = pfx {
+                    if nick == &self.current_nick {
+                        // Set usermask
+                        let usermask = format!("{}!{}", nick, user);
+                        self.usermask = Some(usermask);
+
+                        // Initialize channel state
+                        match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+                            None => {
+                                self.chans.push((chan.to_owned(), HashSet::new()));
+                            }
+                            Some(chan_idx) => {
+                                // This happens because we initialize channel states for channels
+                                // that we will join on connection when the client is first created
+                                self.chans[chan_idx].1.clear();
+                            }
+                        }
+                    } else {
+                        match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+                            Some(chan_idx) => {
+                                self.chans[chan_idx]
+                                    .1
+                                    .insert(wire::drop_nick_prefix(nick).to_owned());
+                            }
+                            None => {
+                                debug!("Can't find channel state for JOIN: {:?}", cmd);
+                            }
+                        }
+                    }
+                }
+            }
+
+            Reply { num: 396, params } => {
+                // :hobana.freenode.net 396 osa1 haskell/developer/osa1
+                // :is now your hidden host (set by services.)
+                if params.len() == 3 {
+                    let usermask = format!(
+                        "{}!~{}@{}",
+                        self.current_nick, self.server_info.hostname, params[1]
+                    );
+                    self.usermask = Some(usermask);
+                }
+            }
+
+            Reply { num: 302, params } => {
+                // 302 RPL_USERHOST
+                // :ircd.stealth.net 302 yournick :syrk=+syrk@millennium.stealth.net
+                //
+                // We know there will be only one nick because /userhost cmd sends
+                // one parameter (our nick)
+                //
+                // Example args: ["osa1", "osa1=+omer@moz-s8a.9ac.93.91.IP "]
+
+                let param = &params[1];
+                match find_byte(param.as_bytes(), b'=') {
+                    None => {
+                        // TODO: Log this
+                    }
+                    Some(mut i) => {
+                        if param.as_bytes().get(i + 1) == Some(&b'+')
+                            || param.as_bytes().get(i + 1) == Some(&b'-')
+                        {
+                            i += 1;
+                        }
+                        let usermask = (&param[i..]).trim();
+                        self.usermask = Some(usermask.to_owned());
+                    }
+                }
+            }
+
+            //
+            // Remove channel state on PART
+            //
+            PART { chan, .. } => {
+                if let Some(Pfx::User { nick, .. }) = pfx {
+                    if nick == &self.current_nick {
+                        match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+                            None => {
+                                debug!("Can't find channel state: {}", chan);
+                            }
+                            Some(chan_idx) => {
+                                self.chans.remove(chan_idx);
+                            }
+                        }
+                    } else {
+                        match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+                            Some(chan_idx) => {
+                                self.chans[chan_idx].1.remove(wire::drop_nick_prefix(nick));
+                            }
+                            None => {
+                                debug!("Can't find channel state for PART: {:?}", cmd);
+                            }
+                        }
+                    }
+                }
+            }
+
+            //
+            // RPL_WELCOME, start introduction sequence and NickServ authentication
+            //
+            Reply { num: 001, .. } => {
+                snd_ev.try_send(Event::Connected).unwrap();
+                snd_ev
+                    .try_send(Event::NickChange(self.current_nick.clone()))
+                    .unwrap();
+                self.nick_accepted = true;
+                if let Some(ref pwd) = self.nickserv_ident {
+                    snd_irc_msg
+                        .try_send(wire::privmsg("NickServ", &format!("identify {}", pwd)))
+                        .unwrap();
+                }
+            }
+
+            //
+            // RPL_YOURHOST, set servername
+            //
+            Reply { num: 002, params } => {
+                // 002    RPL_YOURHOST
+                //        "Your host is <servername>, running version <ver>"
+
+                // An example <servername>: cherryh.freenode.net[149.56.134.238/8001]
+
+                match parse_servername(params) {
+                    None => {
+                        // TODO: Log
+                    }
+                    Some(servername) => {
+                        self.servername = Some(servername);
+                    }
+                }
+            }
+
+            //
+            // ERR_NICKNAMEINUSE, try another nick if we don't have a nick yet
+            //
+            Reply { num: 433, .. } => {
+                // ERR_NICKNAMEINUSE. If we don't have a nick already try next nick.
+                if !self.nick_accepted {
+                    let new_nick = self.get_next_nick();
+                    // debug!("new nick: {}", new_nick);
+                    snd_ev
+                        .try_send(Event::NickChange(new_nick.to_owned()))
+                        .unwrap();
+                    snd_irc_msg.try_send(wire::nick(new_nick)).unwrap();
+                }
+            }
+
+            //
+            // NICK message sent from the server when our nick change request was successful
+            //
+            NICK {
+                nick: new_nick,
+                ref mut chans,
+            } => {
+                if let Some(Pfx::User { nick: old_nick, .. }) = pfx {
+                    if old_nick == &self.current_nick {
+                        snd_ev
+                            .try_send(Event::NickChange(new_nick.to_owned()))
+                            .unwrap();
+                        if !self.nicks.contains(new_nick) {
+                            self.nicks.push(new_nick.to_owned());
+                            self.current_nick_idx = self.nicks.len() - 1;
+                        }
+                    }
+
+                    // Rename the nick in channel states, also populate the chan list
+                    for (chan, nicks) in &mut self.chans {
+                        if nicks.remove(old_nick) {
+                            nicks.insert(new_nick.to_owned());
+                            chans.push(chan.to_owned());
+                        }
+                    }
+                }
+            }
+
+            //
+            // RPL_ENDOFMOTD, join channels, set away status (TODO)
+            //
+            Reply { num: 376, .. } => {
+                let chans: Vec<&str> = self.chans.iter().map(|(s, _)| s.as_str()).collect();
+                if !chans.is_empty() {
+                    snd_irc_msg.try_send(wire::join(&chans)).unwrap();
+                }
+            }
+
+            //
+            // RPL_NAMREPLY: users in a channel
+            //
+            Reply { num: 353, params } => {
+                let chan = &params[2];
+                match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+                    None => self.chans.push((
+                        chan.to_owned(),
+                        params[3]
+                            .split_whitespace()
+                            .map(|s| wire::drop_nick_prefix(s).to_owned())
+                            .collect(),
+                    )),
+                    Some(idx) => {
+                        let nick_set = &mut self.chans[idx].1;
+                        for nick in params[3].split_whitespace() {
+                            nick_set.insert(wire::drop_nick_prefix(nick).to_owned());
+                        }
+                    }
+                }
+            }
+
+            //
+            // QUIT: Update the `chans` field for the channels that the user was in
+            //
+            QUIT { ref mut chans, .. } => {
+                let nick = match pfx {
+                    Some(Pfx::User { nick, .. }) => nick,
+                    _ => {
+                        // TODO: WAT?
+                        return;
+                    }
+                };
+                for (chan, nicks) in self.chans.iter_mut() {
+                    if nicks.contains(nick) {
+                        chans.push(chan.to_owned());
+                        nicks.remove(nick);
+                    }
+                }
+            }
+
+            //
+            // SASL authentication
+            //
+            CAP {
+                client: _,
+                subcommand,
+                params,
+            } => {
+                match subcommand.as_ref() {
+                    "ACK" => {
+                        if params.iter().any(|cap| cap.as_str() == "sasl") {
+                            snd_irc_msg.try_send(wire::authenticate("PLAIN")).unwrap();
+                        }
+                    }
+                    "NAK" => {
+                        snd_irc_msg.try_send(wire::cap_end()).unwrap();
+                    }
+                    "LS" => {
+                        self.introduce(snd_irc_msg);
+                        if params.iter().any(|cap| cap == "sasl") {
+                            snd_irc_msg.try_send(wire::cap_req(&["sasl"])).unwrap();
+                            // Will wait for CAP ... ACK from server before authentication.
+                        }
+                    }
+                    _ => {}
+                }
+            }
+
+            AUTHENTICATE { ref param } => {
+                if param.as_str() == "+" {
+                    // Empty AUTHENTICATE response; server accepted the specified SASL mechanism
+                    // (PLAIN)
+                    if let Some(ref auth) = self.server_info.sasl_auth {
+                        let msg = format!(
+                            "{}\x00{}\x00{}",
+                            auth.username, auth.username, auth.password
+                        );
+                        snd_irc_msg
+                            .try_send(wire::authenticate(&base64::encode(&msg)))
+                            .unwrap();
+                    }
+                }
+            }
+
+            Reply { num: 903, .. } | Reply { num: 904, .. } => {
+                // 903: RPL_SASLSUCCESS, 904: ERR_SASLFAIL
+                snd_irc_msg.try_send(wire::cap_end()).unwrap();
+            }
+
+            //
+            // Ignore the rest
+            //
+            _ => {}
+        }
+    }
+
+    fn get_chan_nicks(&self, chan: &str) -> Vec<String> {
+        match utils::find_idx(&self.chans, |(s, _)| s == chan) {
+            None => vec![], // TODO: Log this, this is probably a bug
+            Some(chan_idx) => self.chans[chan_idx].1.iter().cloned().collect(),
+        }
+    }
+}
+
+/// Try to parse servername in a 002 RPL_YOURHOST reply
+fn parse_servername(params: &[String]) -> Option<String> {
+    let msg = params.get(1).or_else(|| params.get(0))?;
+    let slice1 = &msg[13..];
+    let servername_ends =
+        find_byte(slice1.as_bytes(), b'[').or_else(|| find_byte(slice1.as_bytes(), b','))?;
+    Some((&slice1[..servername_ends]).to_owned())
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_servername_1() {
+        let args = vec![
+            "tiny_test".to_owned(),
+            "Your host is adams.freenode.net[94.125.182.252/8001], \
+             running version ircd-seven-1.1.4"
+                .to_owned(),
+        ];
+        assert_eq!(
+            parse_servername(&args),
+            Some("adams.freenode.net".to_owned())
+        );
+    }
+
+    #[test]
+    fn test_parse_servername_2() {
+        let args = vec![
+            "tiny_test".to_owned(),
+            "Your host is belew.mozilla.org, running version InspIRCd-2.0".to_owned(),
+        ];
+        assert_eq!(
+            parse_servername(&args),
+            Some("belew.mozilla.org".to_owned())
+        );
+    }
+}
diff --git a/libtiny_client/src/stream.rs b/libtiny_client/src/stream.rs
new file mode 100644
index 00000000..c164eb6e
--- /dev/null
+++ b/libtiny_client/src/stream.rs
@@ -0,0 +1,95 @@
+use native_tls;
+use std::{
+    net::SocketAddr,
+    pin::Pin,
+    task::{Context, Poll},
+};
+use tokio::{
+    io::{AsyncRead, AsyncWrite},
+    net::TcpStream,
+};
+use tokio_tls::TlsStream;
+
+#[derive(Debug)]
+pub(crate) enum Stream {
+    TcpStream(TcpStream),
+    TlsStream(TlsStream<TcpStream>),
+}
+
+pub(crate) enum StreamError {
+    TlsError(native_tls::Error),
+    IoError(std::io::Error),
+}
+
+impl From<native_tls::Error> for StreamError {
+    fn from(err: native_tls::Error) -> Self {
+        StreamError::TlsError(err)
+    }
+}
+
+impl From<std::io::Error> for StreamError {
+    fn from(err: std::io::Error) -> Self {
+        StreamError::IoError(err)
+    }
+}
+
+impl Stream {
+    pub(crate) async fn new_tcp(addr: SocketAddr) -> Result<Stream, StreamError> {
+        Ok(Stream::TcpStream(TcpStream::connect(addr).await?))
+    }
+
+    pub(crate) async fn new_tls(addr: SocketAddr, host_name: &str) -> Result<Stream, StreamError> {
+        let tcp_stream = TcpStream::connect(addr).await?;
+        let tls_connector =
+            tokio_tls::TlsConnector::from(native_tls::TlsConnector::builder().build()?);
+        let tls_stream = tls_connector.connect(host_name, tcp_stream).await?;
+        Ok(Stream::TlsStream(tls_stream))
+    }
+}
+
+//
+// Boilerplate
+//
+
+impl AsyncRead for Stream {
+    fn poll_read(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context,
+        buf: &mut [u8],
+    ) -> Poll<Result<usize, std::io::Error>> {
+        match *self {
+            Stream::TcpStream(ref mut tcp_stream) => Pin::new(tcp_stream).poll_read(cx, buf),
+            Stream::TlsStream(ref mut tls_stream) => Pin::new(tls_stream).poll_read(cx, buf),
+        }
+    }
+}
+
+impl AsyncWrite for Stream {
+    fn poll_write(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context,
+        buf: &[u8],
+    ) -> Poll<Result<usize, std::io::Error>> {
+        match *self {
+            Stream::TcpStream(ref mut tcp_stream) => Pin::new(tcp_stream).poll_write(cx, buf),
+            Stream::TlsStream(ref mut tls_stream) => Pin::new(tls_stream).poll_write(cx, buf),
+        }
+    }
+
+    fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<(), std::io::Error>> {
+        match *self {
+            Stream::TcpStream(ref mut tcp_stream) => Pin::new(tcp_stream).poll_flush(cx),
+            Stream::TlsStream(ref mut tls_stream) => Pin::new(tls_stream).poll_flush(cx),
+        }
+    }
+
+    fn poll_shutdown(
+        mut self: Pin<&mut Self>,
+        cx: &mut Context,
+    ) -> Poll<Result<(), std::io::Error>> {
+        match *self {
+            Stream::TcpStream(ref mut tcp_stream) => Pin::new(tcp_stream).poll_shutdown(cx),
+            Stream::TlsStream(ref mut tls_stream) => Pin::new(tls_stream).poll_shutdown(cx),
+        }
+    }
+}
diff --git a/libtiny_client/src/utils.rs b/libtiny_client/src/utils.rs
new file mode 100644
index 00000000..fa898e25
--- /dev/null
+++ b/libtiny_client/src/utils.rs
@@ -0,0 +1,131 @@
+pub(crate) struct SplitIterator<'a> {
+    s: Option<&'a str>,
+    max: usize,
+}
+
+/// Iterate over subslices that are at most `max` long (in bytes). Splits are
+/// made on whitespace characters when possible.
+pub(crate) fn split_iterator(s: &str, max: usize) -> SplitIterator {
+    SplitIterator { s: Some(s), max }
+}
+
+impl<'a> Iterator for SplitIterator<'a> {
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<&'a str> {
+        if self.max == 0 {
+            return None;
+        }
+
+        match self.s {
+            None => None,
+            Some(s) => {
+                if s.len() <= self.max {
+                    let ret = Some(s);
+                    self.s = None;
+                    ret
+                } else {
+                    let mut split = 0;
+
+                    // try to split at a whitespace character
+                    for (ws_idx, ws_char) in s.rmatch_indices(char::is_whitespace) {
+                        if ws_idx <= self.max {
+                            // should we include ws char?
+                            if ws_idx + ws_char.len() <= self.max {
+                                split = ws_idx + ws_char.len();
+                            } else {
+                                split = ws_idx;
+                            }
+                            break;
+                        }
+                    }
+
+                    if split == 0 {
+                        // couldn't split at a whitespace, just split at any char
+                        for i in 0..4 {
+                            if s.is_char_boundary(self.max - i) {
+                                split = self.max - i;
+                                break;
+                            }
+                        }
+                    }
+
+                    if split == 0 {
+                        panic!("Can't split long msg: {:?}", s);
+                    } else {
+                        let ret = Some(&s[0..split]);
+                        self.s = Some(&s[split..]);
+                        ret
+                    }
+                }
+            }
+        }
+    }
+}
+
+pub(crate) fn find_idx<A: Eq, F: Fn(&A) -> bool>(slice: &[A], f: F) -> Option<usize> {
+    for (idx, a) in slice.iter().enumerate() {
+        if f(a) {
+            return Some(idx);
+        }
+    }
+    None
+}
+
+#[cfg(test)]
+mod tests {
+
+    extern crate test;
+
+    use super::*;
+
+    #[test]
+    fn test_split_iterator_1() {
+        let iter = split_iterator("yada yada yada", 5);
+        assert_eq!(
+            iter.into_iter().collect::<Vec<&str>>(),
+            vec!["yada ", "yada ", "yada"]
+        );
+    }
+
+    #[test]
+    fn test_split_iterator_2() {
+        let iter = split_iterator("yada yada yada", 4);
+        assert_eq!(
+            iter.into_iter().collect::<Vec<&str>>(),
+            // weird but OK
+            vec!["yada", " ", "yada", " ", "yada"]
+        );
+    }
+
+    #[test]
+    fn test_split_iterator_3() {
+        let iter = split_iterator("yada yada yada", 3);
+        assert_eq!(
+            iter.into_iter().collect::<Vec<&str>>(),
+            vec!["yad", "a ", "yad", "a ", "yad", "a"]
+        );
+    }
+
+    #[test]
+    fn test_split_iterator_4() {
+        let iter = split_iterator("longwordislong", 3);
+        assert_eq!(
+            iter.into_iter().collect::<Vec<&str>>(),
+            vec!["lon", "gwo", "rdi", "slo", "ng"]
+        );
+    }
+
+    #[test]
+    fn test_split_iterator_5() {
+        let iter = split_iterator("", 3);
+        assert_eq!(iter.into_iter().collect::<Vec<&str>>(), vec![""]);
+    }
+
+    #[test]
+    fn test_split_iterator_6() {
+        let iter = split_iterator("", 0);
+        let ret: Vec<&str> = vec![];
+        assert_eq!(iter.into_iter().collect::<Vec<&str>>(), ret);
+    }
+}
diff --git a/libtiny_gui/Cargo.toml b/libtiny_gui/Cargo.toml
new file mode 100644
index 00000000..c24cab5c
--- /dev/null
+++ b/libtiny_gui/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "libtiny_gui"
+version = "0.1.5"
+authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
+description = "GUI implementation for tiny"
+edition = "2018"
+
+[dependencies]
+gdk = "0.11"
+gio = "0.7"
+glib = "0.8"
+gtk = { version = "0.7", features = ["v3_22_30"] }
diff --git a/libtiny_gui/examples/ex1.rs b/libtiny_gui/examples/ex1.rs
new file mode 100644
index 00000000..5e0d2057
--- /dev/null
+++ b/libtiny_gui/examples/ex1.rs
@@ -0,0 +1,3 @@
+fn main() {
+    libtiny_gui::main()
+}
diff --git a/libtiny_gui/src/lib.rs b/libtiny_gui/src/lib.rs
new file mode 100644
index 00000000..ff3d0735
--- /dev/null
+++ b/libtiny_gui/src/lib.rs
@@ -0,0 +1,30 @@
+use gio::prelude::*;
+use gtk::prelude::*;
+
+pub fn main() {
+    let application = gtk::Application::new(Some("com.github.osa1.tiny"), Default::default())
+        .expect("Initialization failed...");
+
+    application.connect_activate(|app| {
+        build_ui(app);
+    });
+
+    application.run(&std::env::args().collect::<Vec<_>>());
+}
+
+fn build_ui(application: &gtk::Application) {
+    let notebook = gtk::Notebook::new();
+    notebook.set_tab_pos(gtk::PositionType::Bottom);
+
+    let test = gtk::Label::new(Some("just testing"));
+
+    notebook.append_page(&test, None::<&gtk::Widget>);
+
+    let window = gtk::ApplicationWindow::new(application);
+
+    window.set_title("gig");
+    window.set_decorated(false);
+    window.set_default_size(200, 200);
+    window.add(&notebook);
+    window.show_all();
+}
diff --git a/libtiny_logger/Cargo.toml b/libtiny_logger/Cargo.toml
new file mode 100644
index 00000000..7080fa46
--- /dev/null
+++ b/libtiny_logger/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "libtiny_logger"
+version = "0.1.0"
+authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
+description = "Implements a logging UI for tiny"
+edition = "2018"
+
+[dependencies]
+env_logger = "0.7"
+libtiny_ui = { path = "../libtiny_ui" }
+log = "0.4"
+time = "0.1"
diff --git a/libtiny_logger/src/lib.rs b/libtiny_logger/src/lib.rs
new file mode 100644
index 00000000..0ce71989
--- /dev/null
+++ b/libtiny_logger/src/lib.rs
@@ -0,0 +1,380 @@
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::fs;
+use std::fs::{File, OpenOptions};
+use std::io;
+use std::io::Result;
+use std::io::Write;
+use std::path::PathBuf;
+use std::rc::Rc;
+use time;
+use time::Tm;
+
+use libtiny_ui::*;
+
+#[macro_use]
+extern crate log;
+
+#[derive(Clone)]
+pub struct Logger {
+    inner: Rc<RefCell<LoggerInner>>,
+}
+
+impl Logger {
+    pub fn new(log_dir: PathBuf, report_err: Box<dyn Fn(String)>) -> Result<Logger> {
+        Ok(Logger {
+            inner: Rc::new(RefCell::new(LoggerInner::new(log_dir, report_err)?)),
+        })
+    }
+}
+
+macro_rules! delegate {
+    ( $name:ident ( $( $x:ident: $t:ty, )* ) ) => {
+        fn $name(&self, $($x: $t,)*) {
+            self.inner.borrow_mut().$name( $( $x, )* )
+        }
+    }
+}
+
+impl UI for Logger {
+    fn draw(&self) {}
+    delegate!(new_server_tab(serv: &str,));
+    delegate!(close_server_tab(serv: &str,));
+    delegate!(new_chan_tab(serv: &str, chan: &str,));
+    delegate!(close_chan_tab(serv: &str, chan: &str,));
+    delegate!(close_user_tab(serv: &str, nick: &str,));
+    delegate!(add_client_msg(msg: &str, target: &MsgTarget,));
+    delegate!(add_msg(msg: &str, ts: Tm, target: &MsgTarget,));
+    delegate!(add_err_msg(msg: &str, ts: Tm, target: &MsgTarget,));
+    delegate!(add_client_err_msg(msg: &str, target: &MsgTarget,));
+    delegate!(clear_nicks(serv: &str,));
+    delegate!(set_nick(serv: &str, nick: &str,));
+    delegate!(add_privmsg(
+        sender: &str,
+        msg: &str,
+        ts: Tm,
+        target: &MsgTarget,
+        highlight: bool,
+        is_action: bool,
+    ));
+    delegate!(add_nick(nick: &str, ts: Option<Tm>, target: &MsgTarget,));
+    delegate!(remove_nick(nick: &str, ts: Option<Tm>, target: &MsgTarget,));
+    delegate!(rename_nick(
+        old_nick: &str,
+        new_nick: &str,
+        ts: Tm,
+        target: &MsgTarget,
+    ));
+    delegate!(set_topic(topic: &str, ts: Tm, serv: &str, chan: &str,));
+    delegate!(set_tab_style(style: TabStyle, target: &MsgTarget,));
+
+    // TODO: Maybe just return true?
+    fn user_tab_exists(&self, _serv: &str, _nick: &str) -> bool {
+        false
+    }
+}
+
+struct LoggerInner {
+    /// Log file directory
+    log_dir: PathBuf,
+
+    /// Maps server names to their fds
+    servers: HashMap<String, ServerLogs>,
+
+    /// Callback used when reporting errors
+    report_err: Box<dyn Fn(String)>,
+}
+
+struct ServerLogs {
+    fd: File,
+    chans: HashMap<String, File>,
+    users: HashMap<String, File>,
+}
+
+fn print_header(fd: &mut File) -> Result<()> {
+    writeln!(fd)?;
+    writeln!(
+        fd,
+        "*** Logging started at {}",
+        time::strftime("%Y-%m-%d %H:%M:%S", &time::now()).unwrap()
+    )?;
+    writeln!(fd)
+}
+
+macro_rules! report_io_err {
+    ( $f:expr, $e:expr ) => {
+        match $e {
+            Err(err) => {
+                $f(format!("{:?}", err));
+                return;
+            }
+            Ok(ok) => ok,
+        }
+    };
+}
+
+impl LoggerInner {
+    fn new(log_dir: PathBuf, report_err: Box<dyn Fn(String)>) -> Result<LoggerInner> {
+        if let Err(err) = fs::create_dir(&log_dir) {
+            if err.kind() != io::ErrorKind::AlreadyExists {
+                return Err(err);
+            }
+        }
+
+        Ok(LoggerInner {
+            log_dir,
+            servers: HashMap::new(),
+            report_err,
+        })
+    }
+
+    fn new_server_tab(&mut self, serv: &str) {
+        if self.servers.contains_key(serv) {
+            return;
+        }
+
+        let mut path = self.log_dir.clone();
+        path.push(&format!("{}.txt", serv));
+        debug!("Trying to open log file: {:?}", path);
+        let mut fd = report_io_err!(
+            self.report_err,
+            OpenOptions::new().create(true).append(true).open(path)
+        );
+        report_io_err!(self.report_err, print_header(&mut fd));
+
+        self.servers.insert(
+            serv.to_string(),
+            ServerLogs {
+                fd,
+                chans: HashMap::new(),
+                users: HashMap::new(),
+            },
+        );
+    }
+
+    fn close_server_tab(&mut self, serv: &str) {
+        self.servers.remove(serv);
+    }
+
+    fn new_chan_tab(&mut self, serv: &str, chan: &str) {
+        if !self.servers.contains_key(serv) {
+            (self.report_err)(format!("Logger::new_chan_tab: can't find server: {}", serv));
+            return;
+        }
+
+        let server = self.servers.get_mut(serv).unwrap();
+        let mut path = self.log_dir.clone();
+        path.push(&format!("{}_{}.txt", serv, chan));
+        debug!("Trying to open log file: {:?}", path);
+        let mut fd = report_io_err!(
+            self.report_err,
+            OpenOptions::new().create(true).append(true).open(path)
+        );
+        report_io_err!(self.report_err, print_header(&mut fd));
+        server.chans.insert(chan.to_string(), fd);
+    }
+
+    fn close_chan_tab(&mut self, serv: &str, chan: &str) {
+        if !self.servers.contains_key(serv) {
+            (self.report_err)(format!(
+                "Logger::close_chan_tab: can't find server: {}",
+                serv
+            ));
+            return;
+        }
+
+        let server = self.servers.get_mut(serv).unwrap();
+        server.chans.remove(chan);
+    }
+
+    // TODO: Where's new_user_tab?
+
+    fn close_user_tab(&mut self, serv: &str, nick: &str) {
+        if !self.servers.contains_key(serv) {
+            (self.report_err)(format!(
+                "Logger::close_user_tab: can't find server: {}",
+                serv
+            ));
+            return;
+        }
+
+        let server = self.servers.get_mut(serv).unwrap();
+        server.users.remove(nick);
+    }
+
+    fn add_client_msg(&mut self, msg: &str, target: &MsgTarget) {
+        let now = now();
+        self.apply_to_target(target, move |fd: &mut File| {
+            // TODO: Report errors?
+            let _ = writeln!(fd, "[{}] [client] {}", now, msg);
+        });
+    }
+
+    fn add_msg(&mut self, msg: &str, ts: Tm, target: &MsgTarget) {
+        self.apply_to_target(target, |fd: &mut File| {
+            // TODO: Report errors?
+            let _ = writeln!(fd, "[{}] {}", strf(&ts), msg);
+        });
+    }
+
+    fn add_err_msg(&self, _msg: &str, _ts: Tm, _target: &MsgTarget) {
+        // IRC error messages are ignored
+    }
+
+    fn add_client_err_msg(&self, _msg: &str, _target: &MsgTarget) {
+        // Ditto with client error messages
+    }
+
+    fn clear_nicks(&self, _serv: &str) {
+        // Nothing to do here
+    }
+
+    fn set_nick(&self, _serv: &str, _nick: &str) {
+        // Ditto
+    }
+
+    fn add_privmsg(
+        &mut self,
+        sender: &str,
+        msg: &str,
+        ts: Tm,
+        target: &MsgTarget,
+        _highlight: bool,
+        is_action: bool,
+    ) {
+        self.apply_to_target(target, |fd: &mut File| {
+            // TODO: Report errors?
+            if is_action {
+                let _ = writeln!(fd, "[{}] {} {}", strf(&ts), sender, msg);
+            } else {
+                let _ = writeln!(fd, "[{}] {}: {}", strf(&ts), sender, msg);
+            }
+        });
+    }
+
+    fn add_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
+        if let Some(_ts) = ts {
+            // This method is only called when a user joins a chan
+            self.apply_to_target(target, |fd: &mut File| {
+                // TODO: Report errors?
+                let _ = writeln!(fd, "[{}] {} joined the channel.", now(), nick);
+            });
+        }
+    }
+
+    fn remove_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
+        if let Some(_ts) = ts {
+            // TODO: Did the user leave a channel or the server? Currently we can't tell.
+            self.apply_to_target(target, |fd: &mut File| {
+                // TODO: Report errors?
+                let _ = writeln!(fd, "[{}] {} left.", now(), nick);
+            });
+        }
+    }
+
+    fn rename_nick(&mut self, old_nick: &str, new_nick: &str, ts: Tm, target: &MsgTarget) {
+        self.apply_to_target(target, |fd: &mut File| {
+            // TODO: Report errors?
+            let _ = writeln!(
+                fd,
+                "[{}] {} is now known as {}.",
+                strf(&ts),
+                old_nick,
+                new_nick
+            );
+        });
+    }
+
+    fn set_topic(&mut self, topic: &str, ts: Tm, serv: &str, chan: &str) {
+        let target = MsgTarget::Chan { serv, chan };
+        self.apply_to_target(&target, |fd: &mut File| {
+            // TODO: Report errors?
+            let _ = writeln!(fd, "[{}] Channel topic: {}.", strf(&ts), topic);
+        });
+    }
+
+    fn set_tab_style(&self, _: TabStyle, _: &MsgTarget) {
+        // Nothing to do here
+    }
+
+    fn apply_to_target(&mut self, target: &MsgTarget, f: impl Fn(&mut File)) {
+        match *target {
+            MsgTarget::Server { serv } => {
+                if !self.servers.contains_key(serv) {
+                    (self.report_err)(format!("Logger: can't find server: {}", serv));
+                    return;
+                }
+                let ServerLogs { ref mut fd, .. } = self.servers.get_mut(serv).unwrap();
+                f(fd);
+            }
+            MsgTarget::Chan { serv, chan } => {
+                if !self.servers.contains_key(serv) {
+                    (self.report_err)(format!("Logger: can't find server: {}", serv));
+                    return;
+                }
+                let ServerLogs { ref mut chans, .. } = self.servers.get_mut(serv).unwrap();
+                if !chans.contains_key(chan) {
+                    (self.report_err)(format!(
+                        "Logger: can't find chan {} in server {}",
+                        chan, serv
+                    ));
+                    return;
+                }
+                let fd = chans.get_mut(chan).unwrap();
+                f(fd);
+            }
+            MsgTarget::User { serv, nick } => {
+                if !self.servers.contains_key(serv) {
+                    (self.report_err)(format!("Logger: can't find server: {}", serv));
+                    return;
+                }
+                let ServerLogs { ref mut users, .. } = self.servers.get_mut(serv).unwrap();
+                if !users.contains_key(nick) {
+                    // We don't have a `new_user_tab` trait method so user log files are created
+                    // here
+                    let mut path = self.log_dir.clone();
+                    path.push(&format!("{}_{}.txt", serv, nick));
+                    debug!("Trying to open log file: {:?}", path);
+                    let mut fd = report_io_err!(
+                        self.report_err,
+                        OpenOptions::new().create(true).append(true).open(path)
+                    );
+                    report_io_err!(self.report_err, print_header(&mut fd));
+                    users.insert(nick.to_owned(), fd);
+                }
+                let fd = users.get_mut(nick).unwrap();
+                f(fd);
+            }
+            MsgTarget::AllServTabs { serv } => {
+                if !self.servers.contains_key(serv) {
+                    (self.report_err)(format!("Logger: can't find server: {}", serv));
+                    return;
+                }
+                let ServerLogs {
+                    ref mut fd,
+                    ref mut chans,
+                    ref mut users,
+                    ..
+                } = self.servers.get_mut(serv).unwrap();
+                f(fd);
+                for (_, fd) in chans.iter_mut() {
+                    f(fd);
+                }
+                for (_, fd) in users.iter_mut() {
+                    f(fd);
+                }
+            }
+            MsgTarget::CurrentTab => {
+                // Probably a cmd error; these are ignored
+            }
+        }
+    }
+}
+
+fn now() -> String {
+    time::strftime("%H:%M:%S", &time::now()).unwrap()
+}
+
+fn strf(tm: &Tm) -> String {
+    time::strftime("%H:%M:%S", tm).unwrap()
+}
diff --git a/libtiny_tui/Cargo.toml b/libtiny_tui/Cargo.toml
new file mode 100644
index 00000000..55611183
--- /dev/null
+++ b/libtiny_tui/Cargo.toml
@@ -0,0 +1,22 @@
+[package]
+name = "libtiny_tui"
+version = "0.1.0"
+description = "A terminal UI for tiny"
+edition = "2018"
+
+[dependencies]
+env_logger = "0.7"
+futures-util-preview = "0.3.0-alpha.18"
+libtiny_ui = { path = "../libtiny_ui" }
+log = "0.4"
+notify-rust = "3"
+serde = { version = "1.0.8", features = ["derive"] }
+tempfile = "3.0.3"
+term_input = { path = "../term_input" }
+termbox_simple = { path = "../termbox" }
+time = "0.1"
+tokio = { git = "https://github.com/tokio-rs/tokio.git", features = ["timer"] }
+
+[dev-dependencies]
+mio = "0.6.9"
+libc = "0.2.22"
diff --git a/examples/chat.rs b/libtiny_tui/examples/chat.rs
similarity index 83%
rename from examples/chat.rs
rename to libtiny_tui/examples/chat.rs
index 6197120e..d84083d2 100644
--- a/examples/chat.rs
+++ b/libtiny_tui/examples/chat.rs
@@ -1,24 +1,10 @@
 // In a chat window add dozens of nicks, each printing some random lines.
 
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-extern crate time;
-extern crate tiny;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
-use std::fs::File;
-use std::io::Read;
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
+use std::{fs::File, io::Read};
 
+use libtiny_tui::{Colors, MsgTarget, TUIRet, TUI};
 use term_input::{Event, Input};
-use tiny::config::Colors;
-use tiny::tui::{MsgTarget, TUIRet, Timestamp, TUI};
 
 fn main() {
     let chan_target = MsgTarget::Chan {
@@ -29,7 +15,7 @@ fn main() {
     let mut tui = TUI::new(Colors::default());
     tui.new_server_tab("debug");
     tui.new_chan_tab("debug", "chan");
-    tui.show_topic("This is channel topic", Timestamp::now(), &chan_target);
+    tui.show_topic("This is channel topic", time::now(), &chan_target);
     tui.draw();
 
     {
@@ -38,7 +24,7 @@ fn main() {
         file.read_to_string(&mut text).unwrap();
 
         for (line_idx, line) in text.lines().enumerate() {
-            let now = Timestamp::now();
+            let now = time::now();
             let nick = format!("nick_{}", line_idx);
             tui.add_nick(&nick, Some(now), &chan_target);
             tui.add_privmsg(&nick, line, now, &chan_target, false);
@@ -79,7 +65,7 @@ fn main() {
                             } else {
                                 tui.add_msg(
                                     &msg.into_iter().collect::<String>(),
-                                    Timestamp::now(),
+                                    time::now(),
                                     &MsgTarget::Server { serv_name: "debug" },
                                 );
                             }
@@ -90,14 +76,14 @@ fn main() {
                         TUIRet::EventIgnored(Event::FocusGained) => {
                             tui.add_msg(
                                 "focus gained",
-                                Timestamp::now(),
+                                time::now(),
                                 &MsgTarget::Server { serv_name: "debug" },
                             );
                         }
                         TUIRet::EventIgnored(Event::FocusLost) => {
                             tui.add_msg(
                                 "focus lost",
-                                Timestamp::now(),
+                                time::now(),
                                 &MsgTarget::Server { serv_name: "debug" },
                             );
                         }
diff --git a/examples/colors.rs b/libtiny_tui/examples/colors.rs
similarity index 91%
rename from examples/colors.rs
rename to libtiny_tui/examples/colors.rs
index 59decd11..ebdc7298 100644
--- a/examples/colors.rs
+++ b/libtiny_tui/examples/colors.rs
@@ -1,14 +1,4 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
 use term_input::{Event, Input, Key};
 use termbox_simple::*;
 
diff --git a/examples/completion.rs b/libtiny_tui/examples/completion.rs
similarity index 79%
rename from examples/completion.rs
rename to libtiny_tui/examples/completion.rs
index 148db87b..fc5b6eac 100644
--- a/examples/completion.rs
+++ b/libtiny_tui/examples/completion.rs
@@ -1,21 +1,8 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-extern crate tiny;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
-use std::fs::File;
-use std::io::Read;
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
+use std::{fs::File, io::Read};
 
+use libtiny_tui::{Colors, MsgTarget, TUIRet, TUI};
 use term_input::{Event, Input};
-use tiny::config::Colors;
-use tiny::tui::{MsgTarget, TUIRet, Timestamp, TUI};
 
 fn main() {
     let mut tui = TUI::new(Colors::default());
@@ -24,7 +11,7 @@ fn main() {
 
     tui.add_msg(
         "Loading word list for auto-completion ...",
-        Timestamp::now(),
+        time::now(),
         &debug_tab,
     );
     tui.draw();
@@ -38,7 +25,7 @@ fn main() {
         }
     }
 
-    tui.add_msg("Done.", Timestamp::now(), &debug_tab);
+    tui.add_msg("Done.", time::now(), &debug_tab);
     tui.draw();
 
     let poll = Poll::new().unwrap();
@@ -66,7 +53,7 @@ fn main() {
                         TUIRet::Input { msg, .. } => {
                             tui.add_msg(
                                 &msg.into_iter().collect::<String>(),
-                                Timestamp::now(),
+                                time::now(),
                                 &debug_tab,
                             );
                         }
diff --git a/examples/msg_area_example.rs b/libtiny_tui/examples/msg_area_example.rs
similarity index 79%
rename from examples/msg_area_example.rs
rename to libtiny_tui/examples/msg_area_example.rs
index 9dd1651a..d84e9638 100644
--- a/examples/msg_area_example.rs
+++ b/libtiny_tui/examples/msg_area_example.rs
@@ -1,20 +1,7 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-extern crate time;
-extern crate tiny;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
 
+use libtiny_tui::{Colors, MsgTarget, TUIRet, TUI};
 use term_input::{Event, Input};
-use tiny::config::Colors;
-use tiny::tui::{MsgTarget, TUIRet, Timestamp, TUI};
 
 fn main() {
     let mut tui = TUI::new(Colors::default());
@@ -47,7 +34,7 @@ fn main() {
                         TUIRet::Input { msg, .. } => {
                             tui.add_msg(
                                 &msg.into_iter().collect::<String>(),
-                                Timestamp::now(),
+                                time::now(),
                                 &MsgTarget::Server { serv_name: "debug" },
                             );
                         }
diff --git a/examples/tabs.rs b/libtiny_tui/examples/tabs.rs
similarity index 89%
rename from examples/tabs.rs
rename to libtiny_tui/examples/tabs.rs
index 1dde4681..fa1d5b6c 100644
--- a/examples/tabs.rs
+++ b/libtiny_tui/examples/tabs.rs
@@ -1,22 +1,8 @@
 // Open a lot of tabs. 10 servers tabs, each one having 3 channels.
 
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-extern crate tiny;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
+use libtiny_tui::{Colors, MsgSource, MsgTarget, TUIRet, TabStyle, TUI};
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
 use term_input::{Event, Input};
-use tiny::config::Colors;
-use tiny::tui::MsgSource;
-use tiny::tui::TabStyle;
-use tiny::tui::{MsgTarget, TUIRet, Timestamp, TUI};
 
 fn main() {
     let mut tui = TUI::new(Colors::default());
@@ -85,7 +71,7 @@ fn main() {
                                         tui.add_privmsg(
                                             "me",
                                             &msg_string,
-                                            Timestamp::now(),
+                                            time::now(),
                                             &MsgTarget::Chan {
                                                 serv_name: &serv_name,
                                                 chan_name: &chan_name,
diff --git a/examples/text.rs b/libtiny_tui/examples/text.rs
similarity index 89%
rename from examples/text.rs
rename to libtiny_tui/examples/text.rs
index dcaec2ab..4855832e 100644
--- a/examples/text.rs
+++ b/libtiny_tui/examples/text.rs
@@ -1,25 +1,16 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-extern crate tiny;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
-use std::fs::File;
-use std::io::Read;
+use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
+use std::{fs::File, io::Read};
 
 use term_input::{Event, Input, Key};
 use termbox_simple::*;
 
-use tiny::config::Colors;
-use tiny::tui::msg_area::line::SchemeStyle;
-use tiny::tui::msg_area::line::SegStyle;
-use tiny::tui::msg_area::MsgArea;
+use libtiny_tui::{
+    msg_area::{
+        line::{SchemeStyle, SegStyle},
+        MsgArea,
+    },
+    Colors,
+};
 
 fn main() {
     let mut tui = Termbox::init().unwrap();
diff --git a/libtiny_tui/src/config.rs b/libtiny_tui/src/config.rs
new file mode 100644
index 00000000..560d9be9
--- /dev/null
+++ b/libtiny_tui/src/config.rs
@@ -0,0 +1,242 @@
+use serde::{
+    de::{self, Deserializer, MapAccess, Visitor},
+    Deserialize,
+};
+pub use termbox_simple::*;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Style {
+    /// Termbox fg.
+    pub fg: u16,
+
+    /// Termbox bg.
+    pub bg: u16,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(default)]
+pub struct Colors {
+    pub nick: Vec<u8>,
+    pub clear: Style,
+    pub user_msg: Style,
+    pub err_msg: Style,
+    pub topic: Style,
+    pub cursor: Style,
+    pub join: Style,
+    pub part: Style,
+    pub nick_change: Style,
+    pub faded: Style,
+    pub exit_dialogue: Style,
+    pub highlight: Style,
+    pub completion: Style,
+    pub timestamp: Style,
+    pub tab_active: Style,
+    pub tab_normal: Style,
+    pub tab_new_msg: Style,
+    pub tab_highlight: Style,
+    pub statusline_normal: Style,
+    pub statusline_left: Style,
+    pub statusline_right: Style,
+}
+
+impl Default for Colors {
+    fn default() -> Self {
+        Colors {
+            nick: vec![1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14],
+            clear: Style {
+                fg: TB_DEFAULT,
+                bg: TB_DEFAULT,
+            },
+            user_msg: Style {
+                fg: 0,
+                bg: TB_DEFAULT,
+            },
+            err_msg: Style { fg: TB_BOLD, bg: 1 },
+            topic: Style {
+                fg: 14 | TB_BOLD,
+                bg: TB_DEFAULT,
+            },
+            cursor: Style {
+                fg: 0,
+                bg: TB_DEFAULT,
+            },
+            join: Style {
+                fg: 10 | TB_BOLD,
+                bg: TB_DEFAULT,
+            },
+            part: Style {
+                fg: 1 | TB_BOLD,
+                bg: TB_DEFAULT,
+            },
+            nick_change: Style {
+                fg: 10 | TB_BOLD,
+                bg: TB_DEFAULT,
+            },
+            faded: Style {
+                fg: 242,
+                bg: TB_DEFAULT,
+            },
+            exit_dialogue: Style {
+                fg: TB_DEFAULT,
+                bg: 4,
+            },
+            highlight: Style {
+                fg: 9 | TB_BOLD,
+                bg: TB_DEFAULT,
+            },
+            completion: Style {
+                fg: 84,
+                bg: TB_DEFAULT,
+            },
+            timestamp: Style {
+                fg: 242,
+                bg: TB_DEFAULT,
+            },
+            tab_active: Style { fg: TB_BOLD, bg: 0 },
+            tab_normal: Style { fg: 8, bg: 0 },
+            tab_new_msg: Style { fg: 5, bg: 0 },
+            tab_highlight: Style {
+                fg: 9 | TB_BOLD,
+                bg: 0,
+            },
+            statusline_normal: Style { fg: 15, bg: 8 },
+            statusline_left: Style { fg: 10, bg: 8 },
+            statusline_right: Style { fg: 7, bg: 8 },
+        }
+    }
+}
+
+//
+// Parsing
+//
+
+// Color names are taken from https://en.wikipedia.org/wiki/List_of_software_palettes
+const COLORS: [(&str, u16); 17] = [
+    ("default", TB_DEFAULT), // Default fg/bg color of the terminal
+    ("black", 0),
+    ("maroon", 1),
+    ("green", 2),
+    ("olive", 3),
+    ("navy", 4),
+    ("purple", 5),
+    ("teal", 6),
+    ("silver", 7),
+    ("gray", 8),
+    ("red", 9),
+    ("lime", 10),
+    ("yellow", 11),
+    ("blue", 12),
+    ("magenta", 13),
+    ("cyan", 14),
+    ("white", 15),
+];
+
+const ATTRS: [(&str, u16); 2] = [("bold", TB_BOLD), ("underline", TB_UNDERLINE)];
+
+fn parse_color(val: String) -> Option<u16> {
+    for &(name, color) in &COLORS {
+        if val == name {
+            return Some(color);
+        }
+    }
+
+    // If color name doesn't match try get a number
+    val.parse().ok()
+}
+
+fn parse_attr(val: String) -> u16 {
+    for &(name, attr) in &ATTRS {
+        if name == val {
+            return attr;
+        }
+    }
+    0
+}
+
+impl<'de> Deserialize<'de> for Style {
+    fn deserialize<D>(d: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        #[derive(Deserialize)]
+        #[serde(field_identifier, rename_all = "lowercase")]
+        enum Field {
+            Fg,
+            Bg,
+            Attrs,
+        }
+
+        use std::fmt;
+
+        struct StyleVisitor;
+        impl<'de> Visitor<'de> for StyleVisitor {
+            type Value = Style;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                let colors = COLORS
+                    .iter()
+                    .map(|&(name, _)| name)
+                    .collect::<Vec<&str>>()
+                    .join(", ");
+                let attrs = ATTRS
+                    .iter()
+                    .map(|&(name, _)| name)
+                    .collect::<Vec<&str>>()
+                    .join(", ");
+
+                writeln!(
+                    formatter,
+                    "fg: 0-255 or color name\n\
+                     bg: 0-255 or color name\n\
+                     attrs: [{}]\n\n\
+                     color names: {}",
+                    attrs, colors
+                )
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+            where
+                M: MapAccess<'de>,
+            {
+                let mut fg: Option<u16> = None;
+                let mut bg: Option<u16> = None;
+                let mut attr: u16 = 0;
+
+                while let Some(key) = map.next_key()? {
+                    match key {
+                        Field::Fg => {
+                            let color = parse_color(map.next_value()?).ok_or_else(|| {
+                                de::Error::invalid_value(de::Unexpected::UnitVariant, &self)
+                            })?;
+
+                            fg = Some(color);
+                        }
+
+                        Field::Bg => {
+                            let color = parse_color(map.next_value()?).ok_or_else(|| {
+                                de::Error::invalid_value(de::Unexpected::UnitVariant, &self)
+                            })?;
+
+                            bg = Some(color);
+                        }
+
+                        Field::Attrs => {
+                            let attrs: Vec<String> = map.next_value()?;
+                            attr = attrs
+                                .into_iter()
+                                .map(parse_attr)
+                                .fold(0, |style, a| style | a);
+                        }
+                    }
+                }
+
+                let fg = fg.ok_or_else(|| de::Error::missing_field("fg"))?;
+                let bg = bg.ok_or_else(|| de::Error::missing_field("bg"))?;
+
+                Ok(Style { fg: fg | attr, bg })
+            }
+        }
+
+        d.deserialize_map(StyleVisitor)
+    }
+}
diff --git a/src/tui/exit_dialogue.rs b/libtiny_tui/src/exit_dialogue.rs
similarity index 71%
rename from src/tui/exit_dialogue.rs
rename to libtiny_tui/src/exit_dialogue.rs
index 9d8b05a5..dbe6e7c2 100644
--- a/src/tui/exit_dialogue.rs
+++ b/libtiny_tui/src/exit_dialogue.rs
@@ -1,27 +1,26 @@
 use term_input::Key;
 use termbox_simple::Termbox;
 
-use crate::config::Colors;
-use crate::tui::widget::WidgetRet;
+use crate::{config::Colors, widget::WidgetRet};
 
-pub struct ExitDialogue {
+pub(crate) struct ExitDialogue {
     width: i32,
 }
 
 impl ExitDialogue {
-    pub fn new(width: i32) -> ExitDialogue {
+    pub(crate) fn new(width: i32) -> ExitDialogue {
         ExitDialogue { width }
     }
 }
 
-static MSG: &'static str = "Really quit?";
+static MSG: &str = "Really quit?";
 
 impl ExitDialogue {
-    pub fn resize(&mut self, width: i32) {
+    pub(crate) fn resize(&mut self, width: i32) {
         self.width = width;
     }
 
-    pub fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         tb.hide_cursor();
 
         let mut col = 0;
@@ -48,7 +47,7 @@ impl ExitDialogue {
         }
     }
 
-    pub fn keypressed(&self, key: Key) -> WidgetRet {
+    pub(crate) fn keypressed(&self, key: Key) -> WidgetRet {
         match key {
             Key::Char('y') | Key::Char('\r') => WidgetRet::Abort,
             _ => WidgetRet::Remove,
diff --git a/libtiny_tui/src/lib.rs b/libtiny_tui/src/lib.rs
new file mode 100644
index 00000000..df23208a
--- /dev/null
+++ b/libtiny_tui/src/lib.rs
@@ -0,0 +1,170 @@
+#![allow(clippy::too_many_arguments)]
+#![allow(clippy::cognitive_complexity)]
+
+mod config;
+mod exit_dialogue;
+mod messaging;
+// FIXME: This is "pub" to be able to use in an example
+#[doc(hidden)]
+pub mod msg_area;
+mod notifier;
+mod statusline;
+mod tab;
+mod termbox;
+mod text_field;
+mod trie;
+mod tui;
+mod utils;
+mod widget;
+
+pub use crate::config::Colors;
+pub use crate::tab::TabStyle;
+pub use libtiny_ui::*;
+
+use futures_util::stream::StreamExt;
+use std::cell::RefCell;
+use std::rc::{Rc, Weak};
+use term_input::Input;
+use time::Tm;
+use tokio::runtime::current_thread::Runtime;
+use tokio::sync::mpsc;
+
+#[macro_use]
+extern crate log;
+
+#[derive(Clone)]
+pub struct TUI {
+    inner: Weak<RefCell<tui::TUI>>,
+}
+
+impl TUI {
+    pub fn run(colors: Colors, runtime: &mut Runtime) -> (TUI, mpsc::Receiver<Event>) {
+        let tui = Rc::new(RefCell::new(tui::TUI::new(colors)));
+        let inner = Rc::downgrade(&tui);
+
+        let (snd_ev, rcv_ev) = mpsc::channel(10);
+
+        // Spawn input handler task
+        runtime.spawn(input_handler(tui, snd_ev));
+
+        (TUI { inner }, rcv_ev)
+    }
+}
+
+async fn input_handler(tui: Rc<RefCell<tui::TUI>>, mut snd_ev: mpsc::Sender<Event>) {
+    let mut input = Input::new();
+    while let Some(mb_ev) = input.next().await {
+        use tui::TUIRet::*;
+        match mb_ev {
+            Err(io_err) => {
+                debug!("term_input error: {:?}", io_err);
+                return;
+            }
+            Ok(ev) => {
+                let tui_ret = tui.borrow_mut().handle_input_event(ev);
+                match tui_ret {
+                    Abort => {
+                        snd_ev.try_send(Event::Abort).unwrap();
+                        return;
+                    }
+                    KeyHandled | KeyIgnored(_) | EventIgnored(_) => {}
+                    Input { msg, from } => {
+                        if msg[0] == '/' {
+                            // Handle TUI commands, send others to downstream
+                            let cmd: String = (&msg[1..]).iter().collect();
+                            let handled = tui.borrow_mut().try_handle_cmd(&cmd, &from);
+                            if !handled {
+                                snd_ev.try_send(Event::Cmd { cmd, source: from }).unwrap();
+                            }
+                        } else {
+                            snd_ev
+                                .try_send(Event::Msg {
+                                    msg: msg.into_iter().collect(),
+                                    source: from,
+                                })
+                                .unwrap();
+                        }
+                    }
+                    Lines { lines, from } => {
+                        snd_ev
+                            .try_send(Event::Lines {
+                                lines,
+                                source: from,
+                            })
+                            .unwrap();
+                    }
+                }
+            }
+        }
+        tui.borrow_mut().draw();
+    }
+}
+
+macro_rules! delegate {
+    ( $name:ident ( $( $x:ident: $t:ty, )* ) ) => {
+        fn $name(&self, $($x: $t,)*) {
+            if let Some(inner) = self.inner.upgrade() {
+                inner.borrow_mut().$name( $( $x, )* );
+            }
+        }
+    }
+}
+
+macro_rules! delegate_pub {
+    ( $name:ident ( $( $x:ident: $t:ty, )* ) ) => {
+        pub fn $name(&self, $($x: $t,)*) {
+            if let Some(inner) = self.inner.upgrade() {
+                inner.borrow_mut().$name( $( $x, )* );
+            }
+        }
+    }
+}
+
+impl UI for TUI {
+    delegate!(draw());
+    delegate!(new_server_tab(serv_name: &str,));
+    delegate!(close_server_tab(serv_name: &str,));
+    delegate!(new_chan_tab(serv_name: &str, chan: &str,));
+    delegate!(close_chan_tab(serv_name: &str, chan: &str,));
+    delegate!(close_user_tab(serv_name: &str, nick: &str,));
+    delegate!(add_client_msg(msg: &str, target: &MsgTarget,));
+    delegate!(add_msg(msg: &str, ts: Tm, target: &MsgTarget,));
+    delegate!(add_err_msg(msg: &str, ts: Tm, target: &MsgTarget,));
+    delegate!(add_client_err_msg(msg: &str, target: &MsgTarget,));
+    delegate!(clear_nicks(serv_name: &str,));
+    delegate!(set_nick(serv_name: &str, new_nick: &str,));
+    delegate!(add_privmsg(
+        sender: &str,
+        msg: &str,
+        ts: Tm,
+        target: &MsgTarget,
+        highlight: bool,
+        is_action: bool,
+    ));
+    delegate!(add_nick(nick: &str, ts: Option<Tm>, target: &MsgTarget,));
+    delegate!(remove_nick(nick: &str, ts: Option<Tm>, target: &MsgTarget,));
+    delegate!(rename_nick(
+        old_nick: &str,
+        new_nick: &str,
+        ts: Tm,
+        target: &MsgTarget,
+    ));
+    delegate!(set_topic(
+        topic: &str,
+        ts: Tm,
+        serv_name: &str,
+        chan_name: &str,
+    ));
+    delegate!(set_tab_style(style: TabStyle, target: &MsgTarget,));
+
+    fn user_tab_exists(&self, serv_name: &str, nick: &str) -> bool {
+        match self.inner.upgrade() {
+            Some(tui) => tui.borrow().user_tab_exists(serv_name, nick),
+            None => false,
+        }
+    }
+}
+
+impl TUI {
+    delegate_pub!(set_colors(colors: Colors,));
+}
diff --git a/src/tui/messaging.rs b/libtiny_tui/src/messaging.rs
similarity index 85%
rename from src/tui/messaging.rs
rename to libtiny_tui/src/messaging.rs
index 2979c974..fc1f0212 100644
--- a/src/tui/messaging.rs
+++ b/libtiny_tui/src/messaging.rs
@@ -3,24 +3,25 @@ use termbox_simple::Termbox;
 
 use std::convert::From;
 
-use time;
-use time::Tm;
-
-use crate::config;
-use crate::config::Colors;
-use crate::config::Style;
-use crate::trie::Trie;
-use crate::tui::exit_dialogue::ExitDialogue;
-use crate::tui::msg_area::line::SchemeStyle;
-use crate::tui::msg_area::line::SegStyle;
-use crate::tui::msg_area::MsgArea;
-use crate::tui::termbox;
-use crate::tui::text_field::TextField;
-use crate::tui::widget::WidgetRet;
+use time::{self, Tm};
+
+use crate::{
+    config,
+    config::{Colors, Style},
+    exit_dialogue::ExitDialogue,
+    msg_area::{
+        line::{SchemeStyle, SegStyle},
+        MsgArea,
+    },
+    termbox,
+    text_field::TextField,
+    trie::Trie,
+    widget::WidgetRet,
+};
 
 /// A messaging screen is just a text field to type messages and msg area to
 /// show incoming/sent messages.
-pub struct MessagingUI {
+pub(crate) struct MessagingUI {
     /// Incoming and sent messages appear
     msg_area: MsgArea,
 
@@ -48,16 +49,12 @@ pub struct MessagingUI {
 
 /// Like `time::Tm`, but we only care about hour and minute parts.
 #[derive(PartialEq, Eq, Clone, Copy)]
-pub struct Timestamp {
+pub(crate) struct Timestamp {
     hour: i32,
     min: i32,
 }
 
 impl Timestamp {
-    pub fn now() -> Timestamp {
-        Timestamp::from(time::now())
-    }
-
     fn stamp(self, msg_area: &mut MsgArea) {
         msg_area.set_style(SegStyle::SchemeStyle(SchemeStyle::Timestamp));
         msg_area.add_text(&format!("{:02}:{:02} ", self.hour, self.min));
@@ -82,7 +79,7 @@ struct ActivityLine {
 }
 
 impl MessagingUI {
-    pub fn new(width: i32, height: i32, status: bool) -> MessagingUI {
+    pub(crate) fn new(width: i32, height: i32, status: bool) -> MessagingUI {
         MessagingUI {
             msg_area: MsgArea::new(width, height - 1),
             input_field: TextField::new(width),
@@ -98,7 +95,7 @@ impl MessagingUI {
         }
     }
 
-    pub fn set_nick(&mut self, nick: String) {
+    pub(crate) fn set_nick(&mut self, nick: String) {
         self.current_nick = Some(nick);
         // update text field size
         let w = self.width;
@@ -106,7 +103,7 @@ impl MessagingUI {
         self.resize(w, h);
     }
 
-    pub fn get_nick(&self) -> Option<&str> {
+    pub(crate) fn get_nick(&self) -> Option<&str> {
         self.current_nick.as_ref().map(String::as_str)
     }
 
@@ -117,14 +114,14 @@ impl MessagingUI {
         }
     }
 
-    pub fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         self.msg_area.draw(tb, colors, pos_x, pos_y);
 
         if let Some(ref nick) = self.current_nick {
             if self.draw_current_nick {
                 let nick_color = colors.nick[self.get_nick_color(nick) % colors.nick.len()];
                 let style = Style {
-                    fg: nick_color as u16,
+                    fg: u16::from(nick_color),
                     bg: colors.user_msg.bg,
                 };
                 termbox::print_chars(tb, pos_x, pos_y + self.height - 1, style, nick.chars());
@@ -149,7 +146,7 @@ impl MessagingUI {
         }
     }
 
-    pub fn keypressed(&mut self, key: Key) -> WidgetRet {
+    pub(crate) fn keypressed(&mut self, key: Key) -> WidgetRet {
         match key {
             Key::Ctrl('c') => {
                 self.toggle_exit_dialogue();
@@ -212,7 +209,7 @@ impl MessagingUI {
         }
     }
 
-    pub fn resize(&mut self, width: i32, height: i32) {
+    pub(crate) fn resize(&mut self, width: i32, height: i32) {
         self.width = width;
         self.height = height;
         self.msg_area.resize(width, height - 1);
@@ -240,22 +237,18 @@ impl MessagingUI {
         }
     }
 
-    pub fn get_nicks(&self) -> &Trie {
-        &self.nicks
-    }
-
     /// Get contents of the input field and clear it.
-    pub fn flush_input_field(&mut self) -> String {
+    pub(crate) fn flush_input_field(&mut self) -> String {
         self.input_field.flush()
     }
 
     /// Add a line to input field history.
-    pub fn add_input_field_history(&mut self, str: &str) {
+    pub(crate) fn add_input_field_history(&mut self, str: &str) {
         self.input_field.add_history(str)
     }
 
     /// Set input field contents.
-    pub fn set_input_field(&mut self, str: &str) {
+    pub(crate) fn set_input_field(&mut self, str: &str) {
         self.input_field.set(str)
     }
 
@@ -282,7 +275,7 @@ impl MessagingUI {
         self.last_activity_ts = Some(ts);
     }
 
-    pub fn show_topic(&mut self, topic: &str, ts: Timestamp) {
+    pub(crate) fn show_topic(&mut self, topic: &str, ts: Timestamp) {
         self.add_timestamp(ts);
 
         self.msg_area
@@ -292,7 +285,7 @@ impl MessagingUI {
         self.msg_area.flush_line();
     }
 
-    pub fn add_client_err_msg(&mut self, msg: &str) {
+    pub(crate) fn add_client_err_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
         self.msg_area
@@ -301,7 +294,7 @@ impl MessagingUI {
         self.msg_area.flush_line();
     }
 
-    pub fn add_client_notify_msg(&mut self, msg: &str) {
+    pub(crate) fn add_client_notify_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
         self.msg_area
@@ -311,7 +304,7 @@ impl MessagingUI {
         self.reset_activity_line();
     }
 
-    pub fn add_client_msg(&mut self, msg: &str) {
+    pub(crate) fn add_client_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
         self.msg_area
@@ -321,18 +314,18 @@ impl MessagingUI {
         self.reset_activity_line();
     }
 
-    pub fn add_privmsg(
+    pub(crate) fn add_privmsg(
         &mut self,
         sender: &str,
         msg: &str,
         ts: Timestamp,
         highlight: bool,
-        ctcp_action: bool,
+        is_action: bool,
     ) {
         self.reset_activity_line();
         self.add_timestamp(ts);
 
-        if ctcp_action {
+        if is_action {
             self.msg_area
                 .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
             self.msg_area.add_text("** ");
@@ -348,7 +341,7 @@ impl MessagingUI {
         self.msg_area
             .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
 
-        if !ctcp_action {
+        if !is_action {
             self.msg_area.add_char(':');
         }
         self.msg_area.add_char(' ');
@@ -362,7 +355,7 @@ impl MessagingUI {
         self.msg_area.flush_line();
     }
 
-    pub fn add_msg(&mut self, msg: &str, ts: Timestamp) {
+    pub(crate) fn add_msg(&mut self, msg: &str, ts: Timestamp) {
         self.reset_activity_line();
 
         self.add_timestamp(ts);
@@ -372,7 +365,7 @@ impl MessagingUI {
         self.msg_area.flush_line();
     }
 
-    pub fn add_err_msg(&mut self, msg: &str, ts: Timestamp) {
+    pub(crate) fn add_err_msg(&mut self, msg: &str, ts: Timestamp) {
         self.reset_activity_line();
 
         self.add_timestamp(ts);
@@ -382,7 +375,7 @@ impl MessagingUI {
         self.msg_area.flush_line();
     }
 
-    pub fn clear(&mut self) {
+    pub(crate) fn clear(&mut self) {
         self.msg_area.clear();
     }
 
@@ -400,11 +393,11 @@ impl MessagingUI {
 // Keeping nick list up-to-date
 
 impl MessagingUI {
-    pub fn clear_nicks(&mut self) {
+    pub(crate) fn clear_nicks(&mut self) {
         self.nicks.clear();
     }
 
-    pub fn join(&mut self, nick: &str, ts: Option<Timestamp>) {
+    pub(crate) fn join(&mut self, nick: &str, ts: Option<Timestamp>) {
         if self.show_status && !self.nicks.contains(nick) {
             if let Some(ts) = ts {
                 let line_idx = self.get_activity_line_idx(ts);
@@ -421,7 +414,7 @@ impl MessagingUI {
         self.nicks.insert(nick);
     }
 
-    pub fn part(&mut self, nick: &str, ts: Option<Timestamp>) {
+    pub(crate) fn part(&mut self, nick: &str, ts: Option<Timestamp>) {
         self.nicks.remove(nick);
 
         if self.show_status {
@@ -440,7 +433,7 @@ impl MessagingUI {
 
     /// `state` == `None` means toggle
     /// `state` == `Some(state)` means set it to `state`
-    pub fn set_or_toggle_ignore(&mut self, state: Option<bool>) {
+    pub(crate) fn set_or_toggle_ignore(&mut self, state: Option<bool>) {
         self.show_status = state.unwrap_or(!self.show_status);
         if self.show_status {
             self.add_client_notify_msg("Ignore disabled");
@@ -449,11 +442,11 @@ impl MessagingUI {
         }
     }
 
-    pub fn get_ignore_state(&self) -> bool {
+    pub(crate) fn get_ignore_state(&self) -> bool {
         self.show_status
     }
 
-    pub fn nick(&mut self, old_nick: &str, new_nick: &str, ts: Timestamp) {
+    pub(crate) fn nick(&mut self, old_nick: &str, new_nick: &str, ts: Timestamp) {
         self.nicks.remove(old_nick);
         self.nicks.insert(new_nick);
 
@@ -469,10 +462,6 @@ impl MessagingUI {
         });
     }
 
-    pub fn has_nick(&self, nick: &str) -> bool {
-        self.nicks.contains(nick)
-    }
-
     fn reset_activity_line(&mut self) {
         self.last_activity_line = None;
     }
diff --git a/src/tui/msg_area/line.rs b/libtiny_tui/src/msg_area/line.rs
similarity index 86%
rename from src/tui/msg_area/line.rs
rename to libtiny_tui/src/msg_area/line.rs
index ebbeeb7a..4376f8bc 100644
--- a/src/tui/msg_area/line.rs
+++ b/libtiny_tui/src/msg_area/line.rs
@@ -1,15 +1,12 @@
 use std::mem;
-use termbox_simple;
-use termbox_simple::Termbox;
+use termbox_simple::{self, Termbox};
 
-use crate::config;
-use crate::config::Colors;
-use crate::utils::translate_irc_control_chars;
+use crate::{config, config::Colors, utils::translate_irc_control_chars};
 
 /// A single line added to the widget. May be rendered as multiple lines on the
 /// screen.
 #[derive(Debug)]
-pub struct Line {
+pub(crate) struct Line {
     /// Line segments.
     segs: Vec<Seg>,
 
@@ -36,7 +33,7 @@ struct Seg {
 }
 
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub enum SegStyle {
+pub(crate) enum SegStyle {
     /// A specific style. Useful when rendering IRC colors (which should look
     /// the same across color schemes).
     Fixed(config::Style),
@@ -50,54 +47,38 @@ pub enum SegStyle {
 }
 
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub enum SchemeStyle {
-    Clear,
+pub(crate) enum SchemeStyle {
     UserMsg,
     ErrMsg,
     Topic,
-    Cursor,
     Join,
     Part,
     Nick,
     Faded,
-    ExitDialogue,
     Highlight,
-    Completion,
     Timestamp,
-    TabActive,
-    TabNormal,
-    TabNewMsg,
-    TabHighlight,
 }
 
 impl Seg {
-    pub fn style(&self, colors: &Colors) -> config::Style {
+    pub(crate) fn style(&self, colors: &Colors) -> config::Style {
         match self.style {
             SegStyle::Fixed(style) => style,
             SegStyle::Index(idx) => config::Style {
-                fg: colors.nick[idx % colors.nick.len()] as u16,
+                fg: u16::from(colors.nick[idx % colors.nick.len()]),
                 bg: colors.user_msg.bg,
             },
             SegStyle::SchemeStyle(sty) => {
                 use self::SchemeStyle::*;
                 match sty {
-                    Clear => colors.clear,
                     UserMsg => colors.user_msg,
                     ErrMsg => colors.err_msg,
                     Topic => colors.topic,
-                    Cursor => colors.cursor,
                     Join => colors.join,
                     Part => colors.part,
                     Nick => colors.nick_change,
                     Faded => colors.faded,
-                    ExitDialogue => colors.exit_dialogue,
                     Highlight => colors.highlight,
-                    Completion => colors.completion,
                     Timestamp => colors.timestamp,
-                    TabActive => colors.tab_active,
-                    TabNormal => colors.tab_normal,
-                    TabNewMsg => colors.tab_new_msg,
-                    TabHighlight => colors.tab_highlight,
                 }
             }
         }
@@ -108,7 +89,7 @@ impl Seg {
 const TERMBOX_COLOR_PREFIX: char = '\x00';
 
 impl Line {
-    pub fn new() -> Line {
+    pub(crate) fn new() -> Line {
         Line {
             segs: vec![],
             current_seg: Seg {
@@ -120,7 +101,7 @@ impl Line {
         }
     }
 
-    pub fn set_style(&mut self, style: SegStyle) {
+    pub(crate) fn set_style(&mut self, style: SegStyle) {
         // Just update the last segment if it's empty
         if self.current_seg.text.is_empty() {
             self.current_seg.style = style;
@@ -136,7 +117,7 @@ impl Line {
         }
     }
 
-    pub fn add_text(&mut self, str: &str) {
+    pub(crate) fn add_text(&mut self, str: &str) {
         fn push_color(ret: &mut String, irc_fg: u8, irc_bg: Option<u8>) {
             ret.push(TERMBOX_COLOR_PREFIX);
             ret.push(0 as char); // style
@@ -156,8 +137,8 @@ impl Line {
                 let st = iter.next().unwrap() as u8;
                 let fg = iter.next().unwrap() as u8;
                 let bg = iter.next().unwrap() as u8;
-                let fg = ((st as u16) << 8) | (fg as u16);
-                let bg = bg as u16;
+                let fg = (u16::from(st) << 8) | u16::from(fg);
+                let bg = u16::from(bg);
                 let style = config::Style { fg, bg };
                 self.set_style(SegStyle::Fixed(style));
             } else if char > '\x08' {
@@ -170,7 +151,7 @@ impl Line {
         }
     }
 
-    pub fn add_char(&mut self, char: char) {
+    pub(crate) fn add_char(&mut self, char: char) {
         assert_ne!(char, TERMBOX_COLOR_PREFIX);
         if char.is_whitespace() {
             self.splits.push(self.len_chars);
@@ -179,19 +160,15 @@ impl Line {
         self.len_chars += 1;
     }
 
-    pub fn len_chars(&self) -> i32 {
-        self.len_chars
-    }
-
     /// How many lines does this take when rendered? O(n) where n = number of
     /// split positions in the line (i.e. whitespaces).
-    pub fn rendered_height(&self, width: i32) -> i32 {
+    pub(crate) fn rendered_height(&self, width: i32) -> i32 {
         let mut lines: i32 = 1;
         let mut line_start: i32 = 0;
 
         for split_idx in 0..self.splits.len() {
             let char_idx = self.splits[split_idx];
-            // eprintln!("rendered_height: char_idx: {}", char_idx);
+            // debug!("rendered_height: char_idx: {}", char_idx);
             let col = char_idx - line_start;
 
             // How many more chars can we render in this line?
@@ -202,11 +179,11 @@ impl Line {
                 // -1 because we don't need to render the last space.
                 *self.splits.get(split_idx + 1).unwrap_or(&self.len_chars) - 1 - char_idx;
 
-            // eprintln!("rendered_height: slots_in_line: {}, chars_until_next_split: {}",
-            //           slots_in_line, chars_until_next_split);
+            // debug!("rendered_height: slots_in_line: {}, chars_until_next_split: {}",
+            //        slots_in_line, chars_until_next_split);
 
             if (chars_until_next_split as i32) > slots_in_line {
-                // eprintln!("splitting at {}", char_idx);
+                // debug!("splitting at {}", char_idx);
                 lines += 1;
                 line_start = char_idx + 1;
             }
@@ -215,7 +192,7 @@ impl Line {
         lines
     }
 
-    pub fn draw(
+    pub(crate) fn draw(
         &self,
         tb: &mut Termbox,
         colors: &Colors,
@@ -233,7 +210,7 @@ impl Line {
         let mut char_idx: i32 = 0;
 
         let last_seg: [&Seg; 1] = [&self.current_seg];
-        for seg in self.segs.iter().chain(last_seg.into_iter().map(|s| *s)) {
+        for seg in self.segs.iter().chain(last_seg.iter().copied()) {
             let sty = seg.style(colors);
 
             for char in seg.text.chars() {
@@ -250,10 +227,10 @@ impl Line {
                     assert!(*next_split > char_idx);
                     let chars_until_next_split: i32 = *next_split - char_idx;
 
-                    // eprintln!("chars_until_next_split: {},
-                    //           slots_in_line: {}",
-                    //           chars_until_next_split,
-                    //           slots_in_line);
+                    // debug!("chars_until_next_split: {},
+                    //        slots_in_line: {}",
+                    //        chars_until_next_split,
+                    //        slots_in_line);
 
                     if (chars_until_next_split as i32) <= slots_in_line {
                         // keep rendering chars
@@ -320,12 +297,9 @@ fn irc_color_to_termbox(irc_color: u8) -> u8 {
 #[cfg(test)]
 mod tests {
 
-    extern crate test;
-
-    use self::test::Bencher;
     use super::*;
-    use std::fs::File;
-    use std::io::Read;
+    use std::{fs::File, io::Read};
+    use test::Bencher;
 
     #[test]
     fn height_test_1() {
diff --git a/src/tui/msg_area/mod.rs b/libtiny_tui/src/msg_area/mod.rs
similarity index 83%
rename from src/tui/msg_area/mod.rs
rename to libtiny_tui/src/msg_area/mod.rs
index ae35a8a0..52c0d7b8 100644
--- a/src/tui/msg_area/mod.rs
+++ b/libtiny_tui/src/msg_area/mod.rs
@@ -1,16 +1,13 @@
-pub mod line;
+pub(crate) mod line;
 
-use std::cmp::max;
-use std::mem;
-use std::str;
+use std::{cmp::max, mem, str};
 
 use termbox_simple::Termbox;
 
-pub use self::line::Line;
-pub use self::line::SegStyle;
+pub(crate) use self::line::{Line, SegStyle};
 use crate::config::Colors;
 
-pub struct MsgArea {
+pub(crate) struct MsgArea {
     lines: Vec<Line>,
 
     // Rendering related
@@ -30,7 +27,7 @@ pub struct MsgArea {
 }
 
 impl MsgArea {
-    pub fn new(width: i32, height: i32) -> MsgArea {
+    pub(crate) fn new(width: i32, height: i32) -> MsgArea {
         MsgArea {
             lines: Vec::new(),
             width,
@@ -41,13 +38,13 @@ impl MsgArea {
         }
     }
 
-    pub fn resize(&mut self, width: i32, height: i32) {
+    pub(crate) fn resize(&mut self, width: i32, height: i32) {
         self.width = width;
         self.height = height;
         self.lines_height = None;
     }
 
-    pub fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         // Where to render current line
         let mut row = pos_y + self.height - 1;
 
@@ -107,33 +104,33 @@ impl MsgArea {
         }
     }
 
-    pub fn scroll_up(&mut self) {
+    pub(crate) fn scroll_up(&mut self) {
         if self.scroll < max(0, self.lines_height() - self.height) {
             self.scroll += 1;
         }
     }
 
-    pub fn scroll_down(&mut self) {
+    pub(crate) fn scroll_down(&mut self) {
         if self.scroll > 0 {
             self.scroll -= 1;
         }
     }
 
-    pub fn scroll_top(&mut self) {
+    pub(crate) fn scroll_top(&mut self) {
         self.scroll = max(0, self.lines_height() - self.height);
     }
 
-    pub fn scroll_bottom(&mut self) {
+    pub(crate) fn scroll_bottom(&mut self) {
         self.scroll = 0;
     }
 
-    pub fn page_up(&mut self) {
+    pub(crate) fn page_up(&mut self) {
         for _ in 0..10 {
             self.scroll_up();
         }
     }
 
-    pub fn page_down(&mut self) {
+    pub(crate) fn page_down(&mut self) {
         self.scroll = max(0, self.scroll - 10);
     }
 }
@@ -142,19 +139,19 @@ impl MsgArea {
 // Adding/removing text
 
 impl MsgArea {
-    pub fn set_style(&mut self, style: SegStyle) {
+    pub(crate) fn set_style(&mut self, style: SegStyle) {
         self.line_buf.set_style(style);
     }
 
-    pub fn add_text(&mut self, str: &str) {
+    pub(crate) fn add_text(&mut self, str: &str) {
         self.line_buf.add_text(str);
     }
 
-    pub fn add_char(&mut self, char: char) {
+    pub(crate) fn add_char(&mut self, char: char) {
         self.line_buf.add_char(char);
     }
 
-    pub fn flush_line(&mut self) -> usize {
+    pub(crate) fn flush_line(&mut self) -> usize {
         let line_height = self.line_buf.rendered_height(self.width);
         self.lines
             .push(mem::replace(&mut self.line_buf, Line::new()));
@@ -168,14 +165,14 @@ impl MsgArea {
     }
 
     #[inline]
-    pub fn modify_line<F>(&mut self, idx: usize, f: F)
+    pub(crate) fn modify_line<F>(&mut self, idx: usize, f: F)
     where
         F: Fn(&mut Line),
     {
         f(&mut self.lines[idx]);
     }
 
-    pub fn clear(&mut self) {
+    pub(crate) fn clear(&mut self) {
         self.lines.clear();
         self.scroll = 0;
         self.lines_height = None;
diff --git a/src/notifier.rs b/libtiny_tui/src/notifier.rs
similarity index 71%
rename from src/notifier.rs
rename to libtiny_tui/src/notifier.rs
index 714a2666..fb29fc99 100644
--- a/src/notifier.rs
+++ b/libtiny_tui/src/notifier.rs
@@ -1,15 +1,14 @@
-extern crate notify_rust;
-
-use self::notify_rust::Notification;
-pub use crate::tui::messaging::Timestamp;
-use crate::tui::MsgTarget;
-use crate::utils::remove_irc_control_chars;
+use crate::{utils::remove_irc_control_chars, MsgTarget};
+use notify_rust::Notification;
 
 /// Destktop notification handler
 #[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub enum Notifier {
+pub(crate) enum Notifier {
+    /// Notifications are disabled.
     Off,
+    /// Generate notifications only for mentions.
     Mentions,
+    /// Generate notificastions for all messages.
     Messages,
 }
 
@@ -19,7 +18,7 @@ fn notify(summary: &str, body: &str) {
 }
 
 impl Notifier {
-    pub fn notify_privmsg(
+    pub(crate) fn notify_privmsg(
         &mut self,
         sender: &str,
         msg: &str,
@@ -34,9 +33,9 @@ impl Notifier {
         let msg = remove_irc_control_chars(msg);
 
         match *target {
-            MsgTarget::Chan { chan_name, .. } => {
+            MsgTarget::Chan { chan, .. } => {
                 if *self == Notifier::Messages || (*self == Notifier::Mentions && mention) {
-                    notify(&format!("{} in {}", sender, chan_name), &msg)
+                    notify(&format!("{} in {}", sender, chan), &msg)
                 }
             }
             MsgTarget::User {
diff --git a/libtiny_tui/src/statusline.rs b/libtiny_tui/src/statusline.rs
new file mode 100644
index 00000000..ec117bd6
--- /dev/null
+++ b/libtiny_tui/src/statusline.rs
@@ -0,0 +1,81 @@
+use crate::config::Colors;
+use crate::notifier::Notifier;
+use crate::termbox::print_chars;
+use termbox_simple::Termbox;
+
+pub(crate) fn statusline_visible(width: i32, height: i32) -> bool {
+    width >= 13 // min necessary
+        && height >= 6 // arbitrary
+}
+
+pub(crate) fn draw_statusline(
+    tb: &mut Termbox,
+    width: i32,
+    colors: &Colors,
+    visible_name: &str,
+    notifier: &Notifier,
+    ignore_mode: bool,
+) {
+    let notify_state = match notifier {
+        Notifier::Off => "Off",
+        Notifier::Mentions => "Mentions",
+        Notifier::Messages => "Messages",
+    };
+    let ignore_state = if ignore_mode { "Off" } else { "On" };
+
+    let left_pane: String = format!(" {} ", visible_name);
+
+    if width >= left_pane.chars().count() as i32 + 15 {
+        print_chars(
+            tb,
+            0,
+            0,
+            colors.statusline_normal,
+            " ".repeat(width as usize).chars(),
+        );
+        print_chars(tb, 0, 0, colors.statusline_left, left_pane.chars());
+        if width >= left_pane.chars().count() as i32 + 35 {
+            let right_pane = format!(" Notify: {} | Ignore: {} ", notify_state, ignore_state);
+            let spacing_length =
+                width - (right_pane.chars().count() as i32) - (left_pane.chars().count() as i32);
+            print_chars(
+                tb,
+                spacing_length + left_pane.chars().count() as i32,
+                0,
+                colors.statusline_right,
+                right_pane.chars(),
+            );
+        } else {
+            let notify_state_mini: String = notify_state.chars().take(3).collect();
+            let right_pane = format!("N:{} | I:{}", notify_state_mini, ignore_state);
+            let spacing_length =
+                width - (right_pane.chars().count() as i32) - (left_pane.chars().count() as i32);
+            print_chars(
+                tb,
+                spacing_length + left_pane.chars().count() as i32,
+                0,
+                colors.statusline_right,
+                right_pane.chars(),
+            );
+        }
+    } else if width > 15 {
+        print_chars(
+            tb,
+            0,
+            0,
+            colors.statusline_normal,
+            " ".repeat(width as usize).chars(),
+        );
+        let notify_state_mini: String = notify_state.chars().take(3).collect();
+        let right_pane = format!("N:{} | I:{}", notify_state_mini, ignore_state);
+        let spacing_length =
+            width - (right_pane.chars().count() as i32) - (left_pane.chars().count() as i32);
+        print_chars(
+            tb,
+            spacing_length + left_pane.chars().count() as i32,
+            0,
+            colors.statusline_right,
+            right_pane.chars(),
+        );
+    }
+}
diff --git a/libtiny_tui/src/tab.rs b/libtiny_tui/src/tab.rs
new file mode 100644
index 00000000..dbb71328
--- /dev/null
+++ b/libtiny_tui/src/tab.rs
@@ -0,0 +1,74 @@
+pub use libtiny_ui::TabStyle;
+use termbox_simple::{Termbox, TB_UNDERLINE};
+
+use crate::{
+    config::{Colors, Style},
+    messaging::MessagingUI,
+    notifier::Notifier,
+    MsgSource,
+};
+
+pub(crate) struct Tab {
+    pub(crate) widget: MessagingUI,
+    pub(crate) src: MsgSource,
+    pub(crate) style: TabStyle,
+    /// Alt-character to use to switch to this tab.
+    pub(crate) switch: Option<char>,
+    pub(crate) notifier: Notifier,
+}
+
+fn tab_style(style: TabStyle, colors: &Colors) -> Style {
+    match style {
+        TabStyle::Normal => colors.tab_normal,
+        TabStyle::NewMsg => colors.tab_new_msg,
+        TabStyle::Highlight => colors.tab_highlight,
+    }
+}
+
+impl Tab {
+    pub(crate) fn visible_name(&self) -> &str {
+        self.src.visible_name()
+    }
+
+    pub(crate) fn set_style(&mut self, style: TabStyle) {
+        self.style = style;
+    }
+
+    pub(crate) fn update_source<F>(&mut self, f: &F)
+    where
+        F: Fn(&mut MsgSource),
+    {
+        f(&mut self.src)
+    }
+
+    pub(crate) fn width(&self) -> i32 {
+        // TODO: assuming ASCII string here. We should probably switch to a AsciiStr type.
+        self.visible_name().len() as i32
+    }
+
+    pub(crate) fn draw(
+        &self,
+        tb: &mut Termbox,
+        colors: &Colors,
+        mut pos_x: i32,
+        pos_y: i32,
+        active: bool,
+    ) {
+        let style: Style = if active {
+            colors.tab_active
+        } else {
+            tab_style(self.style, colors)
+        };
+
+        let mut switch_drawn = false;
+        for ch in self.visible_name().chars() {
+            if Some(ch) == self.switch && !switch_drawn {
+                tb.change_cell(pos_x, pos_y, ch, style.fg | TB_UNDERLINE, style.bg);
+                switch_drawn = true;
+            } else {
+                tb.change_cell(pos_x, pos_y, ch, style.fg, style.bg);
+            }
+            pos_x += 1;
+        }
+    }
+}
diff --git a/libtiny_tui/src/termbox.rs b/libtiny_tui/src/termbox.rs
new file mode 100644
index 00000000..d49f86f3
--- /dev/null
+++ b/libtiny_tui/src/termbox.rs
@@ -0,0 +1,14 @@
+//! Some utilities for termbox
+
+use crate::config::Style;
+use termbox_simple::Termbox;
+
+pub(crate) fn print_chars<C>(tb: &mut Termbox, mut pos_x: i32, pos_y: i32, style: Style, chars: C)
+where
+    C: Iterator<Item = char>,
+{
+    for char in chars {
+        tb.change_cell(pos_x, pos_y, char, style.fg, style.bg);
+        pos_x += 1;
+    }
+}
diff --git a/src/tui/text_field.rs b/libtiny_tui/src/text_field.rs
similarity index 97%
rename from src/tui/text_field.rs
rename to libtiny_tui/src/text_field.rs
index b2ddaa27..23dfde93 100644
--- a/src/tui/text_field.rs
+++ b/libtiny_tui/src/text_field.rs
@@ -1,20 +1,18 @@
-use std::cmp::{max, min};
-use std::mem;
+use std::{
+    cmp::{max, min},
+    mem,
+};
 
 use term_input::{Arrow, Key};
 use termbox_simple::Termbox;
 
-use crate::config::Colors;
-use crate::trie::Trie;
-use crate::tui::termbox;
-use crate::tui::widget::WidgetRet;
-use crate::utils;
+use crate::{config::Colors, termbox, trie::Trie, utils, widget::WidgetRet};
 
 // TODO: Make these settings
 const SCROLLOFF: i32 = 5;
 const HIST_SIZE: usize = 30;
 
-pub struct TextField {
+pub(crate) struct TextField {
     /// The message that's currently being edited (not yet sent)
     buffer: Vec<char>,
 
@@ -53,7 +51,7 @@ enum Mode {
 }
 
 impl TextField {
-    pub fn new(width: i32) -> TextField {
+    pub(crate) fn new(width: i32) -> TextField {
         TextField {
             buffer: Vec::with_capacity(512),
             cursor: 0,
@@ -64,13 +62,13 @@ impl TextField {
         }
     }
 
-    pub fn resize(&mut self, width: i32) {
+    pub(crate) fn resize(&mut self, width: i32) {
         self.width = width;
         let cursor = self.cursor;
         self.move_cursor(cursor);
     }
 
-    pub fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         match self.mode {
             Mode::Edit => {
                 draw_line(
@@ -152,7 +150,7 @@ impl TextField {
         }
     }
 
-    pub fn keypressed(&mut self, key: Key) -> WidgetRet {
+    pub(crate) fn keypressed(&mut self, key: Key) -> WidgetRet {
         match key {
             Key::Char('\r') => {
                 if self.line_len() > 0 {
@@ -375,17 +373,17 @@ impl TextField {
     }
 
     /// Get contents of the text field and clear it.
-    pub fn flush(&mut self) -> String {
+    pub(crate) fn flush(&mut self) -> String {
         self.cursor = 0;
         self.buffer.drain(..).collect()
     }
 
     /// Add a line to the text field history.
-    pub fn add_history(&mut self, str: &str) {
+    pub(crate) fn add_history(&mut self, str: &str) {
         self.history.push(str.chars().collect());
     }
 
-    pub fn set(&mut self, str: &str) {
+    pub(crate) fn set(&mut self, str: &str) {
         self.mode = Mode::Edit;
         self.buffer = str.chars().collect();
         self.move_cursor_to_end();
@@ -600,7 +598,7 @@ fn draw_line(
 }
 
 impl TextField {
-    pub fn autocomplete(&mut self, dict: &Trie) {
+    pub(crate) fn autocomplete(&mut self, dict: &Trie) {
         if self.in_autocomplete() {
             // AWFUL CODE YO
             self.keypressed(Key::Arrow(Arrow::Up));
@@ -654,8 +652,6 @@ impl TextField {
 #[cfg(test)]
 mod tests {
 
-    extern crate test;
-
     use super::*;
     use term_input::{Arrow, Key};
 
diff --git a/src/trie.rs b/libtiny_tui/src/trie.rs
similarity index 94%
rename from src/trie.rs
rename to libtiny_tui/src/trie.rs
index 4e171db1..39a37ca6 100644
--- a/src/trie.rs
+++ b/libtiny_tui/src/trie.rs
@@ -1,22 +1,22 @@
-pub struct Trie {
+pub(crate) struct Trie {
     vec: Vec<(char, Box<Trie>)>,
     word: bool,
 }
 
 impl Trie {
-    pub fn new() -> Trie {
+    pub(crate) fn new() -> Trie {
         Trie {
             vec: vec![],
             word: false,
         }
     }
 
-    pub fn clear(&mut self) {
+    pub(crate) fn clear(&mut self) {
         self.vec.clear();
         self.word = false;
     }
 
-    pub fn insert(&mut self, str: &str) {
+    pub(crate) fn insert(&mut self, str: &str) {
         let mut trie_ptr: *mut Trie = &mut *self;
         for char in str.chars() {
             trie_ptr = get_char_node_for_insert(trie_ptr, char);
@@ -27,7 +27,7 @@ impl Trie {
         }
     }
 
-    pub fn contains(&self, str: &str) -> bool {
+    pub(crate) fn contains(&self, str: &str) -> bool {
         let mut trie = self;
         for char in str.chars() {
             if let Some(trie_) = get_char_node_for_lookup(trie, char) {
@@ -39,7 +39,7 @@ impl Trie {
         trie.word
     }
 
-    pub fn remove(&mut self, str: &str) {
+    pub(crate) fn remove(&mut self, str: &str) {
         let mut chars = str.chars();
         if let Some(char) = chars.next() {
             if let Ok(idx) = self.vec.binary_search_by(|&(char_, _)| char_.cmp(&char)) {
@@ -58,7 +58,7 @@ impl Trie {
     }
 
     // TODO: We need an Iterator instance instead.
-    pub fn to_strings(&self, prefix: &str) -> Vec<String> {
+    pub(crate) fn to_strings(&self, prefix: &str) -> Vec<String> {
         let mut ret = {
             if self.word {
                 vec![prefix.to_owned()]
@@ -77,7 +77,7 @@ impl Trie {
     }
 
     // TODO: We need an Iterator instance instead.
-    pub fn drop_pfx(&self, prefix: &mut dyn Iterator<Item = char>) -> Vec<String> {
+    pub(crate) fn drop_pfx(&self, prefix: &mut dyn Iterator<Item = char>) -> Vec<String> {
         let mut trie = self;
         for char in prefix {
             if let Some(trie_) = get_char_node_for_lookup(trie, char) {
@@ -123,8 +123,6 @@ fn get_char_node_for_lookup(trie: &Trie, char: char) -> Option<&Trie> {
 #[cfg(test)]
 mod tests {
 
-    extern crate test;
-
     use super::*;
 
     #[test]
@@ -175,12 +173,9 @@ mod tests {
 #[cfg(test)]
 mod benchs {
 
-    extern crate test;
-
     use self::test::Bencher;
     use super::*;
-    use std::fs::File;
-    use std::io::Read;
+    use std::{fs::File, io::Read};
 
     #[bench]
     fn bench_trie_build(b: &mut Bencher) {
diff --git a/src/tui/mod.rs b/libtiny_tui/src/tui.rs
similarity index 72%
rename from src/tui/mod.rs
rename to libtiny_tui/src/tui.rs
index ef225b6e..d17e36ff 100644
--- a/src/tui/mod.rs
+++ b/libtiny_tui/src/tui.rs
@@ -1,27 +1,27 @@
-pub mod exit_dialogue;
-pub mod messaging;
-pub mod msg_area;
-pub mod tab;
-pub mod termbox;
-pub mod text_field;
-pub mod widget;
+#![cfg_attr(test, feature(test))]
+#![allow(clippy::cognitive_complexity)]
+#![allow(clippy::new_without_default)]
+#![allow(clippy::too_many_arguments)]
 
 use std::str;
+use std::str::SplitWhitespace;
+use time::Tm;
 
-pub use self::messaging::Timestamp;
 use crate::config::Colors;
-use crate::config::Style;
 use crate::notifier::Notifier;
-use crate::trie::Trie;
-use crate::tui::messaging::MessagingUI;
-pub use crate::tui::tab::Tab;
-pub use crate::tui::tab::TabStyle;
-use crate::tui::widget::WidgetRet;
+use crate::tab::TabStyle;
+
+use crate::config::Style;
+use crate::messaging::{MessagingUI, Timestamp};
+use crate::statusline::{draw_statusline, statusline_visible};
+use crate::tab::Tab;
+use crate::widget::WidgetRet;
+use crate::{MsgSource, MsgTarget};
 use term_input::{Arrow, Event, Key};
 use termbox_simple::{OutputMode, Termbox};
 
 #[derive(Debug)]
-pub enum TUIRet {
+pub(crate) enum TUIRet {
     Abort,
     KeyHandled,
     KeyIgnored(Key),
@@ -42,56 +42,10 @@ pub enum TUIRet {
     },
 }
 
-/// Target of a message to be shown on TUI.
-pub enum MsgTarget<'a> {
-    Server {
-        serv_name: &'a str,
-    },
-    Chan {
-        serv_name: &'a str,
-        chan_name: &'a str,
-    },
-    User {
-        serv_name: &'a str,
-        nick: &'a str,
-    },
-
-    /// Show the message in all tabs of a server.
-    AllServTabs {
-        serv_name: &'a str,
-    },
-
-    /// Show the message all server tabs that have the user. (i.e. channels,
-    /// privmsg tabs)
-    AllUserTabs {
-        serv_name: &'a str,
-        nick: &'a str,
-    },
-
-    /// Show the message in currently active tab.
-    CurrentTab,
-}
-
-/// Source of a message from the user.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum MsgSource {
-    /// Message sent in a server tab.
-    Serv { serv_name: String },
-
-    /// Message sent in a channel tab.
-    Chan {
-        serv_name: String,
-        chan_name: String,
-    },
-
-    /// Message sent in a privmsg tab.
-    User { serv_name: String, nick: String },
-}
-
 const LEFT_ARROW: char = '<';
 const RIGHT_ARROW: char = '>';
 
-pub struct TUI {
+pub(crate) struct TUI {
     /// Termbox instance
     tb: Termbox,
 
@@ -103,45 +57,15 @@ pub struct TUI {
     width: i32,
     height: i32,
     h_scroll: i32,
-}
-
-impl MsgSource {
-    pub fn serv_name(&self) -> &str {
-        match *self {
-            MsgSource::Serv { ref serv_name }
-            | MsgSource::Chan { ref serv_name, .. }
-            | MsgSource::User { ref serv_name, .. } => serv_name,
-        }
-    }
 
-    pub fn to_target(&self) -> MsgTarget {
-        match *self {
-            MsgSource::Serv { ref serv_name } => MsgTarget::Server { serv_name },
-            MsgSource::Chan {
-                ref serv_name,
-                ref chan_name,
-            } => MsgTarget::Chan {
-                serv_name,
-                chan_name,
-            },
-            MsgSource::User {
-                ref serv_name,
-                ref nick,
-            } => MsgTarget::User { serv_name, nick },
-        }
-    }
-
-    pub fn visible_name(&self) -> &str {
-        match *self {
-            MsgSource::Serv { ref serv_name, .. } => serv_name,
-            MsgSource::Chan { ref chan_name, .. } => chan_name,
-            MsgSource::User { ref nick, .. } => nick,
-        }
-    }
+    /// Do we want to show statusline?
+    show_statusline: bool,
+    /// Is there room for statusline?
+    statusline_visible: bool,
 }
 
 impl TUI {
-    pub fn new(colors: Colors) -> TUI {
+    pub(crate) fn new(colors: Colors) -> TUI {
         let mut tb = Termbox::init().unwrap(); // TODO: check errors
         tb.set_output_mode(OutputMode::Output256);
         tb.set_clear_attributes(colors.clear.fg, colors.clear.bg);
@@ -157,10 +81,113 @@ impl TUI {
             width,
             height,
             h_scroll: 0,
+            show_statusline: false,
+            statusline_visible: statusline_visible(width, height),
+        }
+    }
+
+    fn ignore(&mut self, src: &MsgSource) {
+        match src {
+            MsgSource::Serv { serv } => {
+                self.toggle_ignore(&MsgTarget::AllServTabs { serv: &serv });
+            }
+            MsgSource::Chan { serv, chan } => {
+                self.toggle_ignore(&MsgTarget::Chan {
+                    serv: &serv,
+                    chan: &chan,
+                });
+            }
+            MsgSource::User { serv, nick } => {
+                self.toggle_ignore(&MsgTarget::User {
+                    serv: &serv,
+                    nick: &nick,
+                });
+            }
+        }
+    }
+
+    fn notify(&mut self, words: &mut SplitWhitespace, src: &MsgSource) {
+        let words: Vec<&str> = words.collect();
+
+        let mut show_usage = || {
+            self.add_client_err_msg(
+                "/notify usage: /notify [off|mentions|messages]",
+                &MsgTarget::CurrentTab,
+            )
+        };
+
+        if words.is_empty() {
+            self.show_notify_mode(&MsgTarget::CurrentTab);
+        } else if words.len() != 1 {
+            show_usage();
+        } else {
+            let notifier = match words[0] {
+                "off" => {
+                    self.add_client_notify_msg("Notifications turned off", &MsgTarget::CurrentTab);
+                    Notifier::Off
+                }
+                "mentions" => {
+                    self.add_client_notify_msg(
+                        "Notifications enabled for mentions",
+                        &MsgTarget::CurrentTab,
+                    );
+                    Notifier::Mentions
+                }
+                "messages" => {
+                    self.add_client_notify_msg(
+                        "Notifications enabled for all messages",
+                        &MsgTarget::CurrentTab,
+                    );
+                    Notifier::Messages
+                }
+                _ => {
+                    return show_usage();
+                }
+            };
+            // can't use `MsgSource::to_target` here, `Serv` case is different
+            let tab_target = match src {
+                MsgSource::Serv { ref serv } => MsgTarget::AllServTabs { serv },
+                MsgSource::Chan { ref serv, ref chan } => MsgTarget::Chan { serv, chan },
+                MsgSource::User { ref serv, ref nick } => MsgTarget::User { serv, nick },
+            };
+            self.set_notifier(notifier, &tab_target);
+        }
+    }
+
+    pub(crate) fn try_handle_cmd(&mut self, cmd: &str, src: &MsgSource) -> bool {
+        let mut words = cmd.split_whitespace();
+        match words.next() {
+            Some("clear") => {
+                self.clear(&src.to_target());
+                true
+            }
+            Some("ignore") => {
+                self.ignore(src);
+                true
+            }
+            Some("notify") => {
+                self.notify(&mut words, src);
+                true
+            }
+            Some("switch") => {
+                match words.next() {
+                    Some(s) => self.switch(s),
+                    None => self.add_client_err_msg(
+                        "/switch usage: /switch <tab name>",
+                        &MsgTarget::CurrentTab,
+                    ),
+                }
+                true
+            }
+            Some("statusline") => {
+                self.toggle_statusline();
+                true
+            }
+            _ => false,
         }
     }
 
-    pub fn set_colors(&mut self, colors: Colors) {
+    pub(crate) fn set_colors(&mut self, colors: Colors) {
         self.tb
             .set_clear_attributes(colors.clear.fg, colors.clear.bg);
         self.colors = colors;
@@ -199,10 +226,15 @@ impl TUI {
             ret
         };
 
+        let statusline_height = if self.statusline_visible && self.show_statusline {
+            1
+        } else {
+            0
+        };
         self.tabs.insert(
             idx,
             Tab {
-                widget: MessagingUI::new(self.width, self.height - 1, status),
+                widget: MessagingUI::new(self.width, self.height - 1 - statusline_height, status),
                 src,
                 style: TabStyle::Normal,
                 switch,
@@ -212,14 +244,14 @@ impl TUI {
     }
 
     /// Returns index of the new tab if a new tab is created.
-    pub fn new_server_tab(&mut self, serv_name: &str) -> Option<usize> {
-        match self.find_serv_tab_idx(serv_name) {
+    pub(crate) fn new_server_tab(&mut self, serv: &str) -> Option<usize> {
+        match self.find_serv_tab_idx(serv) {
             None => {
                 let tab_idx = self.tabs.len();
                 self.new_tab(
                     tab_idx,
                     MsgSource::Serv {
-                        serv_name: serv_name.to_owned(),
+                        serv: serv.to_owned(),
                     },
                     true,
                     Notifier::Mentions,
@@ -231,10 +263,9 @@ impl TUI {
     }
 
     /// Closes a server tab and all associated channel tabs.
-    pub fn close_server_tab(&mut self, serv_name: &str) {
-        if let Some(tab_idx) = self.find_serv_tab_idx(serv_name) {
-            self.tabs
-                .retain(|tab: &Tab| tab.src.serv_name() != serv_name);
+    pub(crate) fn close_server_tab(&mut self, serv: &str) {
+        if let Some(tab_idx) = self.find_serv_tab_idx(serv) {
+            self.tabs.retain(|tab: &Tab| tab.src.serv_name() != serv);
             if self.active_idx == tab_idx {
                 self.select_tab(if tab_idx == 0 { 0 } else { tab_idx - 1 });
             }
@@ -242,22 +273,19 @@ impl TUI {
     }
 
     /// Returns index of the new tab if a new tab is created.
-    pub fn new_chan_tab(&mut self, serv_name: &str, chan_name: &str) -> Option<usize> {
-        match self.find_chan_tab_idx(serv_name, chan_name) {
-            None => match self.find_last_serv_tab_idx(serv_name) {
+    pub(crate) fn new_chan_tab(&mut self, serv: &str, chan: &str) -> Option<usize> {
+        match self.find_chan_tab_idx(serv, chan) {
+            None => match self.find_last_serv_tab_idx(serv) {
                 None => {
-                    self.new_server_tab(serv_name);
-                    self.new_chan_tab(serv_name, chan_name)
+                    self.new_server_tab(serv);
+                    self.new_chan_tab(serv, chan)
                 }
                 Some(serv_tab_idx) => {
                     let mut status_val: bool = true;
                     let mut notifier = Notifier::Mentions;
                     for tab in &self.tabs {
-                        if let MsgSource::Serv {
-                            serv_name: ref serv_name_,
-                        } = tab.src
-                        {
-                            if serv_name == serv_name_ {
+                        if let MsgSource::Serv { serv: ref serv_ } = tab.src {
+                            if serv == serv_ {
                                 status_val = tab.widget.get_ignore_state();
                                 notifier = tab.notifier;
                                 break;
@@ -268,8 +296,8 @@ impl TUI {
                     self.new_tab(
                         tab_idx,
                         MsgSource::Chan {
-                            serv_name: serv_name.to_owned(),
-                            chan_name: chan_name.to_owned(),
+                            serv: serv.to_owned(),
+                            chan: chan.to_owned(),
                         },
                         status_val,
                         notifier,
@@ -288,8 +316,8 @@ impl TUI {
         }
     }
 
-    pub fn close_chan_tab(&mut self, serv_name: &str, chan_name: &str) {
-        if let Some(tab_idx) = self.find_chan_tab_idx(serv_name, chan_name) {
+    pub(crate) fn close_chan_tab(&mut self, serv: &str, chan: &str) {
+        if let Some(tab_idx) = self.find_chan_tab_idx(serv, chan) {
             self.tabs.remove(tab_idx);
             if self.active_idx == tab_idx {
                 self.select_tab(if tab_idx == 0 { 0 } else { tab_idx - 1 });
@@ -298,18 +326,18 @@ impl TUI {
     }
 
     /// Returns index of the new tab if a new tab is created.
-    pub fn new_user_tab(&mut self, serv_name: &str, nick: &str) -> Option<usize> {
-        match self.find_user_tab_idx(serv_name, nick) {
-            None => match self.find_last_serv_tab_idx(serv_name) {
+    pub(crate) fn new_user_tab(&mut self, serv: &str, nick: &str) -> Option<usize> {
+        match self.find_user_tab_idx(serv, nick) {
+            None => match self.find_last_serv_tab_idx(serv) {
                 None => {
-                    self.new_server_tab(serv_name);
-                    self.new_user_tab(serv_name, nick)
+                    self.new_server_tab(serv);
+                    self.new_user_tab(serv, nick)
                 }
                 Some(tab_idx) => {
                     self.new_tab(
                         tab_idx + 1,
                         MsgSource::User {
-                            serv_name: serv_name.to_owned(),
+                            serv: serv.to_owned(),
                             nick: nick.to_owned(),
                         },
                         true,
@@ -326,8 +354,8 @@ impl TUI {
         }
     }
 
-    pub fn close_user_tab(&mut self, serv_name: &str, nick: &str) {
-        if let Some(tab_idx) = self.find_user_tab_idx(serv_name, nick) {
+    pub(crate) fn close_user_tab(&mut self, serv: &str, nick: &str) {
+        if let Some(tab_idx) = self.find_user_tab_idx(serv, nick) {
             self.tabs.remove(tab_idx);
             if self.active_idx == tab_idx {
                 self.select_tab(if tab_idx == 0 { 0 } else { tab_idx - 1 });
@@ -335,7 +363,7 @@ impl TUI {
         }
     }
 
-    pub fn handle_input_event(&mut self, ev: Event) -> TUIRet {
+    pub(crate) fn handle_input_event(&mut self, ev: Event) -> TUIRet {
         match ev {
             Event::Resize => {
                 // This never happens, probably because the our select() loop,
@@ -409,19 +437,13 @@ impl TUI {
                             &MsgTarget::CurrentTab,
                         );
                     }
-                    PasteError::PastedCmd => {
-                        self.add_client_err_msg(
-                            "One of the pasted lines looks like a command.",
-                            &MsgTarget::CurrentTab,
-                        );
-                    }
                 }
                 TUIRet::KeyHandled
             }
         }
     }
 
-    pub fn keypressed(&mut self, key: Key) -> TUIRet {
+    fn keypressed(&mut self, key: Key) -> TUIRet {
         match self.tabs[self.active_idx].widget.keypressed(key) {
             WidgetRet::KeyHandled => TUIRet::KeyHandled,
             WidgetRet::KeyIgnored => self.handle_keypress(key),
@@ -495,15 +517,23 @@ impl TUI {
         }
     }
 
-    pub fn resize(&mut self) {
+    pub(crate) fn resize(&mut self) {
         self.tb.resize();
         self.tb.clear();
 
         self.width = self.tb.width();
         self.height = self.tb.height();
 
+        // self.statusline_visible = statusline_visible(self.width, self.height);
+        let statusline_height =
+            if statusline_visible(self.width, self.height) && self.show_statusline {
+                1
+            } else {
+                0
+            };
         for tab in &mut self.tabs {
-            tab.widget.resize(self.width, self.height - 1);
+            tab.widget
+                .resize(self.width, self.height - 1 - statusline_height);
         }
         // scroll the tab bar so that currently active tab is still visible
         let (mut tab_left, mut tab_right) = self.rendered_tabs();
@@ -549,13 +579,9 @@ impl TUI {
             tab_left = tab_left_;
             num_visible = num_visible_;
         }
-    }
 
-    pub fn get_nicks(&self, serv_name: &str, chan_name: &str) -> Option<&Trie> {
-        match self.find_chan_tab_idx(serv_name, chan_name) {
-            None => None,
-            Some(i) => Some(self.tabs[i].widget.get_nicks()),
-        }
+        // redraw after resize
+        self.draw()
     }
 }
 
@@ -599,6 +625,10 @@ impl TUI {
 
     // right one is exclusive
     fn rendered_tabs(&self) -> (usize, usize) {
+        if self.tabs.len() < 1 {
+            return (0, 0);
+        }
+
         let mut i = 0;
 
         {
@@ -641,12 +671,29 @@ impl TUI {
         (i, j)
     }
 
-    pub fn draw(&mut self) {
+    pub(crate) fn draw(&mut self) {
         self.tb.clear();
 
+        let statusline_height = if self.statusline_visible && self.show_statusline {
+            1
+        } else {
+            0
+        };
+
+        if self.show_statusline && self.statusline_visible {
+            draw_statusline(
+                &mut self.tb,
+                self.width,
+                &self.colors,
+                &self.tabs[self.active_idx].visible_name(),
+                &self.tabs[self.active_idx].notifier,
+                self.tabs[self.active_idx].widget.get_ignore_state(),
+            );
+        }
+
         self.tabs[self.active_idx]
             .widget
-            .draw(&mut self.tb, &self.colors, 0, 0);
+            .draw(&mut self.tb, &self.colors, 0, statusline_height);
 
         // decide whether we need to draw left/right arrows in tab bar
         let left_arr = self.draw_left_arrow();
@@ -663,8 +710,8 @@ impl TUI {
         }
 
         // Debugging
-        // eprintln!("number of tabs to draw: {}", tab_right - tab_left);
-        // eprintln!("left_arr: {}, right_arr: {}", left_arr, right_arr);
+        // debug!("number of tabs to draw: {}", tab_right - tab_left);
+        // debug!("left_arr: {}, right_arr: {}", left_arr, right_arr);
 
         // finally draw the tabs
         for (tab_idx, tab) in (&self.tabs[tab_left..tab_right]).iter().enumerate() {
@@ -713,18 +760,18 @@ impl TUI {
         self.tabs[self.active_idx].set_style(TabStyle::Normal);
     }
 
-    pub fn switch(&mut self, string: &str) {
+    pub(crate) fn switch(&mut self, string: &str) {
         let mut next_idx = self.active_idx;
         for (tab_idx, tab) in self.tabs.iter().enumerate() {
             match tab.src {
-                MsgSource::Serv { ref serv_name } => {
-                    if serv_name.contains(string) {
+                MsgSource::Serv { ref serv } => {
+                    if serv.contains(string) {
                         next_idx = tab_idx;
                         break;
                     }
                 }
-                MsgSource::Chan { ref chan_name, .. } => {
-                    if chan_name.contains(string) {
+                MsgSource::Chan { ref chan, .. } => {
+                    if chan.contains(string) {
                         next_idx = tab_idx;
                         break;
                     }
@@ -842,21 +889,16 @@ impl TUI {
     where
         F: Fn(&mut Tab, bool),
     {
-        // Creating a vector just to make borrow checker happy. Borrow checker
-        // sucks once more. Here it sucks 2x, I can't even create a Vec<&mut Tab>,
-        // I need a Vec<usize>.
-        //
-        // (I could use an array on stack but whatever)
+        // Creating a vector just to make borrow checker happy (I can't have a Vec<&mut Tab>)
+        // I need to collect tabs here because of the "create if not exists" logic.
+        // (see `target_idxs.is_empty()` below)
         let mut target_idxs: Vec<usize> = Vec::with_capacity(1);
 
         match *target {
-            MsgTarget::Server { serv_name } => {
+            MsgTarget::Server { serv } => {
                 for (tab_idx, tab) in self.tabs.iter().enumerate() {
-                    if let MsgSource::Serv {
-                        serv_name: ref serv_name_,
-                    } = tab.src
-                    {
-                        if serv_name == serv_name_ {
+                    if let MsgSource::Serv { serv: ref serv_ } = tab.src {
+                        if serv == serv_ {
                             target_idxs.push(tab_idx);
                             break;
                         }
@@ -864,17 +906,14 @@ impl TUI {
                 }
             }
 
-            MsgTarget::Chan {
-                serv_name,
-                chan_name,
-            } => {
+            MsgTarget::Chan { serv, chan } => {
                 for (tab_idx, tab) in self.tabs.iter().enumerate() {
                     if let MsgSource::Chan {
-                        serv_name: ref serv_name_,
-                        chan_name: ref chan_name_,
+                        serv: ref serv_,
+                        chan: ref chan_,
                     } = tab.src
                     {
-                        if serv_name == serv_name_ && chan_name == chan_name_ {
+                        if serv == serv_ && chan == chan_ {
                             target_idxs.push(tab_idx);
                             break;
                         }
@@ -882,14 +921,14 @@ impl TUI {
                 }
             }
 
-            MsgTarget::User { serv_name, nick } => {
+            MsgTarget::User { serv, nick } => {
                 for (tab_idx, tab) in self.tabs.iter().enumerate() {
                     if let MsgSource::User {
-                        serv_name: ref serv_name_,
+                        serv: ref serv_,
                         nick: ref nick_,
                     } = tab.src
                     {
-                        if serv_name == serv_name_ && nick == nick_ {
+                        if serv == serv_ && nick == nick_ {
                             target_idxs.push(tab_idx);
                             break;
                         }
@@ -897,38 +936,14 @@ impl TUI {
                 }
             }
 
-            MsgTarget::AllServTabs { serv_name } => {
+            MsgTarget::AllServTabs { serv } => {
                 for (tab_idx, tab) in self.tabs.iter().enumerate() {
-                    if tab.src.serv_name() == serv_name {
+                    if tab.src.serv_name() == serv {
                         target_idxs.push(tab_idx);
                     }
                 }
             }
 
-            MsgTarget::AllUserTabs { serv_name, nick } => {
-                for (tab_idx, tab) in self.tabs.iter().enumerate() {
-                    match tab.src {
-                        MsgSource::Serv { .. } => {}
-                        MsgSource::Chan {
-                            serv_name: ref serv_name_,
-                            ..
-                        } => {
-                            if serv_name_ == serv_name && tab.widget.has_nick(nick) {
-                                target_idxs.push(tab_idx);
-                            }
-                        }
-                        MsgSource::User {
-                            serv_name: ref serv_name_,
-                            nick: ref nick_,
-                        } => {
-                            if serv_name_ == serv_name && nick_ == nick {
-                                target_idxs.push(tab_idx);
-                            }
-                        }
-                    }
-                }
-            }
-
             MsgTarget::CurrentTab => {
                 target_idxs.push(self.active_idx);
             }
@@ -948,22 +963,19 @@ impl TUI {
 
     fn maybe_create_tab(&mut self, target: &MsgTarget) -> Option<usize> {
         match *target {
-            MsgTarget::Server { serv_name } | MsgTarget::AllServTabs { serv_name } => {
-                self.new_server_tab(serv_name)
+            MsgTarget::Server { serv } | MsgTarget::AllServTabs { serv } => {
+                self.new_server_tab(serv)
             }
 
-            MsgTarget::Chan {
-                serv_name,
-                chan_name,
-            } => self.new_chan_tab(serv_name, chan_name),
+            MsgTarget::Chan { serv, chan } => self.new_chan_tab(serv, chan),
 
-            MsgTarget::User { serv_name, nick } => self.new_user_tab(serv_name, nick),
+            MsgTarget::User { serv, nick } => self.new_user_tab(serv, nick),
 
             _ => None,
         }
     }
 
-    pub fn set_tab_style(&mut self, style: TabStyle, target: &MsgTarget) {
+    pub(crate) fn set_tab_style(&mut self, style: TabStyle, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, is_active: bool| {
             if !is_active && tab.style < style {
                 tab.set_style(style);
@@ -973,7 +985,7 @@ impl TUI {
 
     /// An error message coming from Tiny, probably because of a command error
     /// etc. Those are not timestamped and not logged.
-    pub fn add_client_err_msg(&mut self, msg: &str, target: &MsgTarget) {
+    pub(crate) fn add_client_err_msg(&mut self, msg: &str, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
             tab.widget.add_client_err_msg(msg);
         });
@@ -981,7 +993,7 @@ impl TUI {
 
     /// A notify message coming from tiny, usually shows a response of a command
     /// e.g. "Notifications enabled".
-    pub fn add_client_notify_msg(&mut self, msg: &str, target: &MsgTarget) {
+    pub(crate) fn add_client_notify_msg(&mut self, msg: &str, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
             tab.widget.add_client_notify_msg(msg);
         });
@@ -989,7 +1001,7 @@ impl TUI {
 
     /// A message from client, usually just to indidate progress, e.g.
     /// "Connecting...". Not timestamed and not logged.
-    pub fn add_client_msg(&mut self, msg: &str, target: &MsgTarget) {
+    pub(crate) fn add_client_msg(&mut self, msg: &str, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
             tab.widget.add_client_msg(msg);
         });
@@ -997,92 +1009,78 @@ impl TUI {
 
     /// privmsg is a message coming from a server or client. Shown with sender's
     /// nick/name and receive time and logged.
-    pub fn add_privmsg(
-        &mut self,
-        sender: &str,
-        msg: &str,
-        ts: Timestamp,
-        target: &MsgTarget,
-        ctcp_action: bool,
-    ) {
-        self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.add_privmsg(sender, msg, ts, false, ctcp_action);
-            let nick = tab.widget.get_nick();
-            if let Some(nick_) = nick {
-                tab.notifier
-                    .notify_privmsg(sender, msg, target, nick_, false);
-            }
-        });
-    }
-
-    /// Similar to `add_privmsg`, except the whole message is highlighted.
-    pub fn add_privmsg_highlight(
+    pub(crate) fn add_privmsg(
         &mut self,
         sender: &str,
         msg: &str,
-        ts: Timestamp,
+        ts: Tm,
         target: &MsgTarget,
-        ctcp_action: bool,
+        highlight: bool,
+        is_action: bool,
     ) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.add_privmsg(sender, msg, ts, true, ctcp_action);
+            tab.widget
+                .add_privmsg(sender, msg, Timestamp::from(ts), highlight, is_action);
             let nick = tab.widget.get_nick();
             if let Some(nick_) = nick {
                 tab.notifier
-                    .notify_privmsg(sender, msg, target, nick_, true);
+                    .notify_privmsg(sender, msg, target, nick_, highlight);
             }
         });
     }
 
     /// A message without any explicit sender info. Useful for e.g. in server
     /// and debug log tabs. Timestamped and logged.
-    pub fn add_msg(&mut self, msg: &str, ts: Timestamp, target: &MsgTarget) {
+    pub(crate) fn add_msg(&mut self, msg: &str, ts: Tm, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.add_msg(msg, ts);
+            tab.widget.add_msg(msg, Timestamp::from(ts));
         });
     }
 
     /// Error messages related with the protocol - e.g. can't join a channel,
     /// nickname is in use etc. Timestamped and logged.
-    pub fn add_err_msg(&mut self, msg: &str, ts: Timestamp, target: &MsgTarget) {
+    pub(crate) fn add_err_msg(&mut self, msg: &str, ts: Tm, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.add_err_msg(msg, ts);
+            tab.widget.add_err_msg(msg, Timestamp::from(ts));
         });
     }
 
-    pub fn show_topic(&mut self, title: &str, ts: Timestamp, target: &MsgTarget) {
-        self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.show_topic(title, ts);
+    pub(crate) fn set_topic(&mut self, title: &str, ts: Tm, serv: &str, chan: &str) {
+        let target = MsgTarget::Chan { serv, chan };
+        self.apply_to_target(&target, &|tab: &mut Tab, _| {
+            tab.widget.show_topic(title, Timestamp::from(ts));
         });
     }
 
-    pub fn clear_nicks(&mut self, target: &MsgTarget) {
-        self.apply_to_target(target, &|tab: &mut Tab, _| {
+    pub(crate) fn clear_nicks(&mut self, serv: &str) {
+        let target = MsgTarget::AllServTabs { serv };
+        self.apply_to_target(&target, &|tab: &mut Tab, _| {
             tab.widget.clear_nicks();
         });
     }
 
-    pub fn add_nick(&mut self, nick: &str, ts: Option<Timestamp>, target: &MsgTarget) {
+    pub(crate) fn add_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.join(nick, ts);
+            tab.widget.join(nick, ts.map(Timestamp::from));
         });
     }
 
-    pub fn remove_nick(&mut self, nick: &str, ts: Option<Timestamp>, target: &MsgTarget) {
+    pub(crate) fn remove_nick(&mut self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.part(nick, ts);
+            tab.widget.part(nick, ts.map(Timestamp::from));
         });
     }
 
-    pub fn rename_nick(
+    pub(crate) fn rename_nick(
         &mut self,
         old_nick: &str,
         new_nick: &str,
-        ts: Timestamp,
+        ts: Tm,
         target: &MsgTarget,
     ) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
-            tab.widget.nick(old_nick, new_nick, ts);
+            tab.widget.nick(old_nick, new_nick, Timestamp::from(ts));
+            // TODO: Does this actually rename the tab?
             tab.update_source(&|src: &mut MsgSource| {
                 if let MsgSource::User { ref mut nick, .. } = *src {
                     nick.clear();
@@ -1092,26 +1090,28 @@ impl TUI {
         });
     }
 
-    pub fn set_nick(&mut self, serv_name: &str, new_nick: &str) {
-        let target = MsgTarget::AllServTabs { serv_name };
+    pub(crate) fn set_nick(&mut self, serv: &str, new_nick: &str) {
+        let target = MsgTarget::AllServTabs { serv };
         self.apply_to_target(&target, &|tab: &mut Tab, _| {
             tab.widget.set_nick(new_nick.to_owned())
         });
     }
 
-    pub fn clear(&mut self, target: &MsgTarget) {
+    pub(crate) fn clear(&mut self, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| tab.widget.clear());
     }
 
-    pub fn toggle_ignore(&mut self, target: &MsgTarget) {
-        if let MsgTarget::AllServTabs { serv_name } = *target {
+    pub(crate) fn toggle_statusline(&mut self) {
+        self.show_statusline = !self.show_statusline;
+        self.resize();
+    }
+
+    pub(crate) fn toggle_ignore(&mut self, target: &MsgTarget) {
+        if let MsgTarget::AllServTabs { serv } = *target {
             let mut status_val: bool = false;
             for tab in &self.tabs {
-                if let MsgSource::Serv {
-                    serv_name: ref serv_name_,
-                } = tab.src
-                {
-                    if serv_name == serv_name_ {
+                if let MsgSource::Serv { serv: ref serv_ } = tab.src {
+                    if serv == serv_ {
                         status_val = tab.widget.get_ignore_state();
                         break;
                     }
@@ -1125,20 +1125,13 @@ impl TUI {
                 tab.widget.set_or_toggle_ignore(None);
             });
         }
-        // Changing tab names (adding "[i]" suffix) may make the tab currently
-        // selected overflow from the screen. Easiest (although not most
-        // efficient) way to fix this is `resize()`.
-        self.resize();
     }
 
-    pub fn does_user_tab_exist(&self, serv_name_: &str, nick_: &str) -> bool {
+    // TODO: Maybe remove this and add a `create: bool` field to MsgTarget::User
+    pub(crate) fn user_tab_exists(&self, serv_: &str, nick_: &str) -> bool {
         for tab in &self.tabs {
-            if let MsgSource::User {
-                ref serv_name,
-                ref nick,
-            } = tab.src
-            {
-                if serv_name_ == serv_name && nick_ == nick {
+            if let MsgSource::User { ref serv, ref nick } = tab.src {
+                if serv_ == serv && nick_ == nick {
                     return true;
                 }
             }
@@ -1146,13 +1139,13 @@ impl TUI {
         false
     }
 
-    pub fn set_notifier(&mut self, notifier: Notifier, target: &MsgTarget) {
+    pub(crate) fn set_notifier(&mut self, notifier: Notifier, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
             tab.notifier = notifier;
         });
     }
 
-    pub fn show_notify_mode(&mut self, target: &MsgTarget) {
+    pub(crate) fn show_notify_mode(&mut self, target: &MsgTarget) {
         self.apply_to_target(target, &|tab: &mut Tab, _| {
             let msg = match tab.notifier {
                 Notifier::Off => "Notifications are off",
@@ -1166,10 +1159,10 @@ impl TUI {
     ////////////////////////////////////////////////////////////////////////////
     // Helpers
 
-    fn find_serv_tab_idx(&self, serv_name_: &str) -> Option<usize> {
+    fn find_serv_tab_idx(&self, serv_: &str) -> Option<usize> {
         for (tab_idx, tab) in self.tabs.iter().enumerate() {
-            if let MsgSource::Serv { ref serv_name } = tab.src {
-                if serv_name_ == serv_name {
+            if let MsgSource::Serv { ref serv } = tab.src {
+                if serv_ == serv {
                     return Some(tab_idx);
                 }
             }
@@ -1177,14 +1170,10 @@ impl TUI {
         None
     }
 
-    fn find_chan_tab_idx(&self, serv_name_: &str, chan_name_: &str) -> Option<usize> {
+    fn find_chan_tab_idx(&self, serv_: &str, chan_: &str) -> Option<usize> {
         for (tab_idx, tab) in self.tabs.iter().enumerate() {
-            if let MsgSource::Chan {
-                ref serv_name,
-                ref chan_name,
-            } = tab.src
-            {
-                if serv_name_ == serv_name && chan_name_ == chan_name {
+            if let MsgSource::Chan { ref serv, ref chan } = tab.src {
+                if serv_ == serv && chan_ == chan {
                     return Some(tab_idx);
                 }
             }
@@ -1192,14 +1181,10 @@ impl TUI {
         None
     }
 
-    fn find_user_tab_idx(&self, serv_name_: &str, nick_: &str) -> Option<usize> {
+    fn find_user_tab_idx(&self, serv_: &str, nick_: &str) -> Option<usize> {
         for (tab_idx, tab) in self.tabs.iter().enumerate() {
-            if let MsgSource::User {
-                ref serv_name,
-                ref nick,
-            } = tab.src
-            {
-                if serv_name_ == serv_name && nick_ == nick {
+            if let MsgSource::User { ref serv, ref nick } = tab.src {
+                if serv_ == serv && nick_ == nick {
                     return Some(tab_idx);
                 }
             }
@@ -1208,9 +1193,9 @@ impl TUI {
     }
 
     /// Index of the last tab with the given server name.
-    fn find_last_serv_tab_idx(&self, serv_name: &str) -> Option<usize> {
+    fn find_last_serv_tab_idx(&self, serv: &str) -> Option<usize> {
         for (tab_idx, tab) in self.tabs.iter().enumerate().rev() {
-            if tab.src.serv_name() == serv_name {
+            if tab.src.serv_name() == serv {
                 return Some(tab_idx);
             }
         }
@@ -1240,11 +1225,9 @@ impl TUI {
 }
 
 #[derive(Debug)]
-pub enum PasteError {
+enum PasteError {
     Io(::std::io::Error),
     Var(::std::env::VarError),
-    /// One of the lines looks like a command. This is something we don't support yet.
-    PastedCmd,
 }
 
 impl From<::std::io::Error> for PasteError {
@@ -1270,11 +1253,11 @@ impl From<::std::env::VarError> for PasteError {
 ///
 /// FIXME: Ideally this function should get a `Termbox` argument and return a new `Termbox` because
 /// we shutdown the current termbox instance and initialize it again after running $EDITOR.
-pub fn paste_lines(tf: String, str: &str) -> Result<Vec<String>, PasteError> {
-    use std::io::Read;
-    use std::io::Write;
-    use std::io::{Seek, SeekFrom};
-    use std::process::Command;
+fn paste_lines(tf: String, str: &str) -> Result<Vec<String>, PasteError> {
+    use std::{
+        io::{Read, Seek, SeekFrom, Write},
+        process::Command,
+    };
 
     use termbox_simple::*;
 
diff --git a/libtiny_tui/src/utils.rs b/libtiny_tui/src/utils.rs
new file mode 100644
index 00000000..d7c5ec86
--- /dev/null
+++ b/libtiny_tui/src/utils.rs
@@ -0,0 +1,154 @@
+pub(crate) struct InsertIterator<'iter, A: 'iter> {
+    insert_point: usize,
+    current_idx: usize,
+    iter_orig: &'iter mut dyn Iterator<Item = A>,
+    iter_insert: &'iter mut dyn Iterator<Item = A>,
+}
+
+impl<'iter, A> Iterator for InsertIterator<'iter, A> {
+    type Item = A;
+
+    fn next(&mut self) -> Option<A> {
+        if self.current_idx >= self.insert_point {
+            if let Some(a) = self.iter_insert.next() {
+                Some(a)
+            } else {
+                self.iter_orig.next()
+            }
+        } else {
+            self.current_idx += 1;
+            self.iter_orig.next()
+        }
+    }
+}
+
+pub(crate) fn insert_iter<'iter, A>(
+    iter_orig: &'iter mut dyn Iterator<Item = A>,
+    iter_insert: &'iter mut dyn Iterator<Item = A>,
+    insert_point: usize,
+) -> InsertIterator<'iter, A> {
+    InsertIterator {
+        insert_point,
+        current_idx: 0,
+        iter_orig,
+        iter_insert,
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+pub(crate) fn is_nick_char(c: char) -> bool {
+    // from RFC 2812:
+    //
+    // nickname   =  ( letter / special ) *8( letter / digit / special / "-" )
+    // special    =  %x5B-60 / %x7B-7D
+    //                  ; "[", "]", "\", "`", "_", "^", "{", "|", "}"
+    //
+    // we use a simpler check here (allows strictly more nicks)
+
+    c.is_alphanumeric()
+        || (c as i32 >= 0x5B && c as i32 <= 0x60)
+        || (c as i32 >= 0x7B && c as i32 <= 0x7D)
+        || c == '-' // not valid according to RFC 2812 but servers accept it and I've seen nicks with
+                    // this char in the wild
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+use std::{iter::Peekable, str::Chars};
+
+/// Parse at least one, at most two digits. Does not consume the iterator when
+/// result is `None`.
+fn parse_color_code(chars: &mut Peekable<Chars>) -> Option<u8> {
+    fn to_dec(ch: char) -> Option<u8> {
+        ch.to_digit(10).map(|c| c as u8)
+    }
+
+    let c1_char = *chars.peek()?;
+    let c1_digit = match to_dec(c1_char) {
+        None => {
+            return None;
+        }
+        Some(c1_digit) => {
+            chars.next();
+            c1_digit
+        }
+    };
+
+    match chars.peek().cloned() {
+        None => Some(c1_digit),
+        Some(c2) => match to_dec(c2) {
+            None => Some(c1_digit),
+            Some(c2_digit) => {
+                chars.next();
+                Some(c1_digit * 10 + c2_digit)
+            }
+        },
+    }
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+/// Translate IRC color codes using the callback, and remove ASCII control chars from the input.
+pub(crate) fn translate_irc_control_chars(
+    str: &str,
+    push_color: fn(ret: &mut String, fg: u8, bg: Option<u8>),
+) -> String {
+    let mut ret = String::with_capacity(str.len());
+    let mut iter = str.chars().peekable();
+
+    while let Some(char) = iter.next() {
+        if char == '\x03' {
+            match parse_color_code(&mut iter) {
+                None => {
+                    // just skip the control char
+                }
+                Some(fg) => {
+                    if let Some(char) = iter.peek().cloned() {
+                        if char == ',' {
+                            iter.next(); // consume ','
+                            match parse_color_code(&mut iter) {
+                                None => {
+                                    // comma was not part of the color code,
+                                    // add it to the new string
+                                    push_color(&mut ret, fg, None);
+                                    ret.push(char);
+                                }
+                                Some(bg) => {
+                                    push_color(&mut ret, fg, Some(bg));
+                                }
+                            }
+                        } else {
+                            push_color(&mut ret, fg, None);
+                        }
+                    } else {
+                        push_color(&mut ret, fg, None);
+                    }
+                }
+            }
+        } else if !char.is_ascii_control() {
+            ret.push(char);
+        }
+    }
+
+    ret
+}
+
+/// Like `translate_irc_control_chars`, but skips color codes.
+pub(crate) fn remove_irc_control_chars(str: &str) -> String {
+    fn push_color(_ret: &mut String, _fg: u8, _bg: Option<u8>) {}
+    translate_irc_control_chars(str, push_color)
+}
+
+#[test]
+fn test_translate_irc_control_chars() {
+    assert_eq!(
+        remove_irc_control_chars("  Le Voyageur imprudent  "),
+        "  Le Voyageur imprudent  "
+    );
+    assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo");
+    assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo");
+    assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo");
+    assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo");
+    assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo");
+}
diff --git a/src/tui/widget.rs b/libtiny_tui/src/widget.rs
similarity index 91%
rename from src/tui/widget.rs
rename to libtiny_tui/src/widget.rs
index cdea741a..83debb07 100644
--- a/src/tui/widget.rs
+++ b/libtiny_tui/src/widget.rs
@@ -1,4 +1,4 @@
-pub enum WidgetRet {
+pub(crate) enum WidgetRet {
     /// Key is handled by the widget.
     KeyHandled,
 
diff --git a/libtiny_ui/Cargo.toml b/libtiny_ui/Cargo.toml
new file mode 100644
index 00000000..fd5614dd
--- /dev/null
+++ b/libtiny_ui/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "libtiny_ui"
+version = "0.1.0"
+authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
+description = "Defines the interface for the UI implementations for tiny"
+edition = "2018"
+
+[dependencies]
+objekt = "0.1"
+time = "0.1"
diff --git a/libtiny_ui/src/lib.rs b/libtiny_ui/src/lib.rs
new file mode 100644
index 00000000..25949354
--- /dev/null
+++ b/libtiny_ui/src/lib.rs
@@ -0,0 +1,307 @@
+pub use objekt::clone_box;
+use time::Tm;
+
+/// Target of a message to be shown in a UI.
+pub enum MsgTarget<'a> {
+    /// Show the message in the server tab.
+    Server { serv: &'a str },
+
+    /// Show the message in the channel tab.
+    Chan { serv: &'a str, chan: &'a str },
+
+    /// Show the message in the privmsg tab.
+    User { serv: &'a str, nick: &'a str },
+
+    /// Show the message in all tabs of a server.
+    AllServTabs { serv: &'a str },
+
+    /// Show the message in currently active tab.
+    CurrentTab,
+}
+
+/// Source of a message from the user.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum MsgSource {
+    /// Message sent in a server tab.
+    Serv { serv: String },
+
+    /// Message sent in a channel tab.
+    Chan { serv: String, chan: String },
+
+    /// Message sent in a privmsg tab.
+    User { serv: String, nick: String },
+}
+
+impl MsgSource {
+    pub fn serv_name(&self) -> &str {
+        match *self {
+            MsgSource::Serv { ref serv }
+            | MsgSource::Chan { ref serv, .. }
+            | MsgSource::User { ref serv, .. } => serv,
+        }
+    }
+
+    pub fn to_target(&self) -> MsgTarget {
+        match *self {
+            MsgSource::Serv { ref serv } => MsgTarget::Server { serv },
+            MsgSource::Chan { ref serv, ref chan } => MsgTarget::Chan { serv, chan },
+            MsgSource::User { ref serv, ref nick } => MsgTarget::User { serv, nick },
+        }
+    }
+
+    pub fn visible_name(&self) -> &str {
+        match *self {
+            MsgSource::Serv { ref serv, .. } => serv,
+            MsgSource::Chan { ref chan, .. } => chan,
+            MsgSource::User { ref nick, .. } => nick,
+        }
+    }
+}
+
+// NOTE: Keep the variants sorted in increasing significance, to avoid updating
+// style with higher significance for a less significant style (e.g. updating
+// from `Highlight` to `NewMsg` in `set_tab_style`).
+// TODO: This shouldn't be a part of the API
+#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
+pub enum TabStyle {
+    Normal,
+    NewMsg,
+    Highlight,
+}
+
+/// UI events
+#[derive(Debug)]
+pub enum Event {
+    Abort,
+    Msg {
+        msg: String,
+        source: MsgSource,
+    },
+    Lines {
+        lines: Vec<String>,
+        source: MsgSource,
+    },
+    Cmd {
+        cmd: String,
+        source: MsgSource,
+    },
+}
+
+pub trait UI: objekt::Clone {
+    fn draw(&self);
+
+    /// Create a new server tab.
+    fn new_server_tab(&self, serv: &str);
+
+    /// Close a server tab and all channel and user tabs for that server.
+    fn close_server_tab(&self, serv: &str);
+
+    /// Create a channel tab in the given server.
+    fn new_chan_tab(&self, serv: &str, chan: &str);
+
+    /// Close a channel tab.
+    fn close_chan_tab(&self, serv: &str, chan: &str);
+
+    /// Close a user tab.
+    fn close_user_tab(&self, serv: &str, nick: &str);
+
+    /// Show a message coming from a client in the given tabs.
+    fn add_client_msg(&self, msg: &str, target: &MsgTarget);
+
+    /// Show a plain message with a timestamp in the given tabs.
+    // TODO: What is a "plain message"? Not descriptive.
+    fn add_msg(&self, msg: &str, ts: Tm, target: &MsgTarget);
+
+    /// Show an IRC error message in the given tabs.
+    fn add_err_msg(&self, msg: &str, ts: Tm, target: &MsgTarget);
+
+    /// Show a client error message in the given tabs. Usuallys generated in case of a command
+    /// error, e.g. "unknown command".
+    fn add_client_err_msg(&self, msg: &str, target: &MsgTarget);
+
+    /// Clear all nicks in a server from the UI's cache. Does not show anything.
+    fn clear_nicks(&self, serv: &str);
+
+    /// Set the client's nick in the given server.
+    fn set_nick(&self, serv: &str, nick: &str);
+
+    /// Show a user message sent to the client or to a channel.
+    ///
+    /// - highlight: Whether to highlight the message. Usually set `true` when the message mentions
+    ///   the client's nick.
+    ///
+    /// - is_action: `true` when this is a CTCP ACTION message.
+    ///
+    fn add_privmsg(
+        &self,
+        sender: &str,
+        msg: &str,
+        ts: Tm,
+        target: &MsgTarget,
+        highlight: bool,
+        is_action: bool,
+    );
+
+    /// Add a nick to the given tabs. When `ts` is not provided this does not show anything; just
+    /// updated the channel nick list etc. Otherwise this shows a line like "foo joined channel".
+    fn add_nick(&self, nick: &str, ts: Option<Tm>, target: &MsgTarget);
+
+    /// Remove a nick from given tabs. Similar to `add_nick`, when `ts` is not provided this does
+    /// not show a "foo left channel" line.
+    fn remove_nick(&self, nick: &str, ts: Option<Tm>, target: &MsgTarget);
+
+    /// Rename a nick in the given tabs.
+    fn rename_nick(&self, old_nick: &str, new_nick: &str, ts: Tm, target: &MsgTarget);
+
+    /// Set topic of given tabs.
+    fn set_topic(&self, topic: &str, ts: Tm, serv: &str, chan: &str);
+
+    /// Set style of the given tabs.
+    fn set_tab_style(&self, style: TabStyle, target: &MsgTarget);
+
+    /// Do we have a tab for the given user? This is useful for deciding where to show a PRIVMSG
+    /// coming from server; e.g. messages from services sometimes shown in their own tabs,
+    /// sometimes in the server tab.
+    fn user_tab_exists(&self, serv: &str, nick: &str) -> bool;
+}
+
+#[derive(Clone)]
+pub struct CombinedUIs<UI1, UI2> {
+    ui1: UI1,
+    ui2: UI2,
+}
+
+impl<UI1: UI + Clone, UI2: UI + Clone> UI for CombinedUIs<UI1, UI2> {
+    fn draw(&self) {
+        self.ui1.draw();
+        self.ui2.draw();
+    }
+
+    fn new_server_tab(&self, serv: &str) {
+        self.ui1.new_server_tab(serv);
+        self.ui2.new_server_tab(serv);
+    }
+
+    fn close_server_tab(&self, serv: &str) {
+        self.ui1.close_server_tab(serv);
+        self.ui2.close_server_tab(serv);
+    }
+
+    fn new_chan_tab(&self, serv: &str, chan: &str) {
+        self.ui1.new_chan_tab(serv, chan);
+        self.ui2.new_chan_tab(serv, chan);
+    }
+
+    fn close_chan_tab(&self, serv: &str, chan: &str) {
+        self.ui1.close_chan_tab(serv, chan);
+        self.ui2.close_chan_tab(serv, chan);
+    }
+
+    fn close_user_tab(&self, serv: &str, nick: &str) {
+        self.ui1.close_user_tab(serv, nick);
+        self.ui2.close_user_tab(serv, nick);
+    }
+
+    fn add_client_msg(&self, msg: &str, target: &MsgTarget) {
+        self.ui1.add_client_msg(msg, target);
+        self.ui2.add_client_msg(msg, target);
+    }
+
+    fn add_msg(&self, msg: &str, ts: Tm, target: &MsgTarget) {
+        self.ui1.add_msg(msg, ts, target);
+        self.ui2.add_msg(msg, ts, target);
+    }
+
+    fn add_err_msg(&self, msg: &str, ts: Tm, target: &MsgTarget) {
+        self.ui1.add_err_msg(msg, ts, target);
+        self.ui2.add_err_msg(msg, ts, target);
+    }
+
+    fn add_client_err_msg(&self, msg: &str, target: &MsgTarget) {
+        self.ui1.add_client_err_msg(msg, target);
+        self.ui2.add_client_err_msg(msg, target);
+    }
+
+    fn clear_nicks(&self, serv: &str) {
+        self.ui1.clear_nicks(serv);
+        self.ui2.clear_nicks(serv);
+    }
+
+    fn set_nick(&self, serv: &str, nick: &str) {
+        self.ui1.set_nick(serv, nick);
+        self.ui2.set_nick(serv, nick);
+    }
+
+    fn add_privmsg(
+        &self,
+        sender: &str,
+        msg: &str,
+        ts: Tm,
+        target: &MsgTarget,
+        highlight: bool,
+        is_action: bool,
+    ) {
+        self.ui1
+            .add_privmsg(sender, msg, ts, target, highlight, is_action);
+        self.ui2
+            .add_privmsg(sender, msg, ts, target, highlight, is_action);
+    }
+
+    fn add_nick(&self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
+        self.ui1.add_nick(nick, ts, target);
+        self.ui2.add_nick(nick, ts, target);
+    }
+
+    fn remove_nick(&self, nick: &str, ts: Option<Tm>, target: &MsgTarget) {
+        self.ui1.remove_nick(nick, ts, target);
+        self.ui2.remove_nick(nick, ts, target);
+    }
+
+    fn rename_nick(&self, old_nick: &str, new_nick: &str, ts: Tm, target: &MsgTarget) {
+        self.ui1.rename_nick(old_nick, new_nick, ts, target);
+        self.ui2.rename_nick(old_nick, new_nick, ts, target);
+    }
+
+    fn set_topic(&self, topic: &str, ts: Tm, serv: &str, chan: &str) {
+        self.ui1.set_topic(topic, ts, serv, chan);
+        self.ui2.set_topic(topic, ts, serv, chan);
+    }
+
+    fn set_tab_style(&self, style: TabStyle, target: &MsgTarget) {
+        self.ui1.set_tab_style(style, target);
+        self.ui2.set_tab_style(style, target);
+    }
+
+    fn user_tab_exists(&self, serv: &str, nick: &str) -> bool {
+        // TODO weird
+        self.ui1.user_tab_exists(serv, nick)
+    }
+}
+
+impl UI for () {
+    fn draw(&self) {}
+    fn new_server_tab(&self, _: &str) {}
+    fn close_server_tab(&self, _: &str) {}
+    fn new_chan_tab(&self, _: &str, _: &str) {}
+    fn close_chan_tab(&self, _: &str, _: &str) {}
+    fn close_user_tab(&self, _: &str, _: &str) {}
+    fn add_client_msg(&self, _: &str, _: &MsgTarget) {}
+    fn add_msg(&self, _: &str, _: Tm, _: &MsgTarget) {}
+    fn add_err_msg(&self, _: &str, _: Tm, _: &MsgTarget) {}
+    fn add_client_err_msg(&self, _: &str, _: &MsgTarget) {}
+    fn clear_nicks(&self, _: &str) {}
+    fn set_nick(&self, _: &str, _: &str) {}
+    fn add_privmsg(&self, _: &str, _: &str, _: Tm, _: &MsgTarget, _: bool, _: bool) {}
+    fn add_nick(&self, _: &str, _: Option<Tm>, _: &MsgTarget) {}
+    fn remove_nick(&self, _: &str, _: Option<Tm>, _: &MsgTarget) {}
+    fn rename_nick(&self, _: &str, _: &str, _: Tm, _: &MsgTarget) {}
+    fn set_topic(&self, _: &str, _: Tm, _: &str, _: &str) {}
+    fn set_tab_style(&self, _: TabStyle, _: &MsgTarget) {}
+    fn user_tab_exists(&self, _: &str, _: &str) -> bool {
+        false
+    }
+}
+
+pub fn combine<UI1: UI, UI2: UI>(ui1: UI1, ui2: UI2) -> CombinedUIs<UI1, UI2> {
+    CombinedUIs { ui1, ui2 }
+}
diff --git a/libtiny_wire/Cargo.toml b/libtiny_wire/Cargo.toml
new file mode 100644
index 00000000..d61cd401
--- /dev/null
+++ b/libtiny_wire/Cargo.toml
@@ -0,0 +1,6 @@
+[package]
+name = "libtiny_wire"
+version = "0.1.0"
+authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
+edition = "2018"
+description = "IRC message parsing and generation"
diff --git a/libtiny_wire/src/lib.rs b/libtiny_wire/src/lib.rs
new file mode 100644
index 00000000..0cc04089
--- /dev/null
+++ b/libtiny_wire/src/lib.rs
@@ -0,0 +1,655 @@
+#![allow(clippy::write_with_newline)]
+
+//! IRC wire protocol message parsers and generators. Incomplete; new messages are added as needed.
+
+use std::str;
+
+pub fn pass(pass: &str) -> String {
+    format!("PASS {}\r\n", pass)
+}
+
+// FIXME: Option<String> because going from Option<String> to Option<&str> is too painful...
+pub fn quit(reason: Option<String>) -> String {
+    match reason {
+        None => "QUIT\r\n".to_string(),
+        Some(reason) => format!("QUIT :{}\r\n", reason),
+    }
+}
+
+pub fn user(hostname: &str, realname: &str) -> String {
+    format!("USER {} 8 * :{}\r\n", hostname, realname)
+}
+
+pub fn nick(arg: &str) -> String {
+    format!("NICK {}\r\n", arg)
+}
+
+pub fn ping(arg: &str) -> String {
+    format!("PING {}\r\n", arg)
+}
+
+pub fn pong(arg: &str) -> String {
+    format!("PONG {}\r\n", arg)
+}
+
+pub fn join(chans: &[&str]) -> String {
+    format!("JOIN {}\r\n", chans.join(","))
+}
+
+pub fn part(channel: &str) -> String {
+    format!("PART {}\r\n", channel)
+}
+
+pub fn privmsg(msgtarget: &str, msg: &str) -> String {
+    // IRC messages need to be shorter than 512 bytes (see RFC 1459 or 2812). This should be dealt
+    // with at call sites as we can't show how we split messages into multiple messages in the UI
+    // at this point.
+    assert!(msgtarget.len() + msg.len() + 12 <= 512);
+    format!("PRIVMSG {} :{}\r\n", msgtarget, msg)
+}
+
+pub fn action(msgtarget: &str, msg: &str) -> String {
+    assert!(msgtarget.len() + msg.len() + 21 <= 512); // See comments in `privmsg`
+    format!("PRIVMSG {} :\x01ACTION {}\x01\r\n", msgtarget, msg)
+}
+
+pub fn away(msg: Option<&str>) -> String {
+    match msg {
+        None => "AWAY\r\n".to_string(),
+        Some(msg) => format!("AWAY :{}\r\n", msg),
+    }
+}
+
+pub fn cap_ls() -> String {
+    "CAP LS\r\n".to_string()
+}
+
+pub fn cap_req(cap_identifiers: &[&str]) -> String {
+    format!("CAP REQ :{}\r\n", cap_identifiers.join(" "))
+}
+
+pub fn cap_end() -> String {
+    "CAP END\r\n".to_string()
+}
+
+pub fn authenticate(msg: &str) -> String {
+    format!("AUTHENTICATE {}\r\n", msg)
+}
+
+/// Sender of a message
+///
+/// `<prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]`
+///
+/// From RFC 2812:
+///
+/// > If the prefix is missing from the message, it is assumed to have originated from the
+/// > connection from which it was received from.
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum Pfx {
+    Server(String),
+
+    /// <nick>!<user>@<host>
+    User {
+        nick: String,
+        /// user@host
+        user: String,
+    },
+}
+
+/// Target of a message
+#[derive(Debug, PartialEq, Eq)]
+pub enum MsgTarget {
+    Chan(String),
+    User(String),
+}
+
+/// An IRC message
+#[derive(Debug, PartialEq, Eq)]
+pub struct Msg {
+    pub pfx: Option<Pfx>,
+    pub cmd: Cmd,
+}
+
+/// An IRC command or reply
+#[derive(Debug, PartialEq, Eq)]
+pub enum Cmd {
+    /// A PRIVMSG or NOTICE. Check `is_notice` field.
+    PRIVMSG {
+        // TODO: In theory this should be a list of targets, but in practice I've never
+        // encountered that case.
+        target: MsgTarget,
+        msg: String,
+        is_notice: bool,
+        is_action: bool,
+    },
+
+    JOIN {
+        // TODO: Same as above, this should be a list ...
+        chan: String, // TODO: key field might be useful when joining restricted channels. In practice I've never
+                      // needed it.
+    },
+
+    PART {
+        // TODO: List of channels
+        chan: String,
+        msg: Option<String>,
+    },
+
+    QUIT {
+        msg: Option<String>,
+        /// Channels of the user that just quit.
+        chans: Vec<String>,
+    },
+
+    NICK {
+        nick: String,
+        /// Channels of the user.
+        chans: Vec<String>,
+    },
+
+    PING {
+        server: String,
+    },
+
+    PONG {
+        server: String,
+    },
+
+    ERROR {
+        msg: String,
+    },
+
+    TOPIC {
+        chan: String,
+        topic: String,
+    },
+
+    CAP {
+        client: String,
+        subcommand: String,
+        params: Vec<String>,
+    },
+
+    AUTHENTICATE {
+        param: String,
+    },
+
+    /// An IRC message other than the ones listed above.
+    Other {
+        cmd: String,
+        params: Vec<String>,
+    },
+
+    /// Numeric replies are kept generic as there are just too many replies and we probably only
+    /// need to handle a small subset of them.
+    Reply {
+        num: u16,
+        params: Vec<String>,
+    },
+}
+
+/// An intermediate type used during parsing.
+enum MsgType<'a> {
+    Cmd(&'a str),
+    Num(u16),
+}
+
+static CRLF: [u8; 2] = [b'\r', b'\n'];
+
+/// Try to read an IRC message off a buffer. Drops the message when parsing is successful.
+/// Otherwise the buffer is left unchanged.
+pub fn parse_irc_msg(buf: &mut Vec<u8>) -> Option<Msg> {
+    // find "\r\n" separator. `IntoSearcher` implementation for slice needs `str` (why??) so
+    // using this hacky method instead.
+    let crlf_idx = {
+        match buf.windows(2).position(|sub| sub == CRLF) {
+            None => return None,
+            Some(i) => i,
+        }
+    };
+
+    let ret = {
+        let mut slice: &[u8] = &buf[0..crlf_idx];
+
+        let pfx: Option<Pfx> = {
+            if slice[0] == b':' {
+                // parse prefix
+                let ws_idx = find_byte(slice, b' ').unwrap();
+                let (mut pfx, slice_) = slice.split_at(ws_idx);
+                // drop the : from pfx
+                pfx = &pfx[1..];
+                slice = &slice_[1..]; // drop the space
+                Some(parse_pfx(pfx))
+            } else {
+                None
+            }
+        };
+
+        let msg_ty: MsgType = {
+            let ws_idx = find_byte(slice, b' ').unwrap();
+            let (cmd, slice_) = slice.split_at(ws_idx);
+            slice = &slice_[1..]; // drop the space
+            match parse_reply_num(cmd) {
+                None => MsgType::Cmd(unsafe {
+                    // Cmd strings are added by the server and they're always ASCII strings, so
+                    // this is safe and O(1).
+                    str::from_utf8_unchecked(cmd)
+                }),
+                Some(num) => MsgType::Num(num),
+            }
+        };
+
+        let params: Vec<&str> = parse_params(unsafe { str::from_utf8_unchecked(slice) });
+        let cmd = match msg_ty {
+            MsgType::Cmd("PRIVMSG") | MsgType::Cmd("NOTICE") if params.len() == 2 => {
+                let is_notice = if let MsgType::Cmd("NOTICE") = msg_ty {
+                    true
+                } else {
+                    false
+                };
+                let target = params[0];
+                let msg = params[1];
+                let target = if target.chars().nth(0) == Some('#') {
+                    MsgTarget::Chan(target.to_owned())
+                } else {
+                    MsgTarget::User(target.to_owned())
+                };
+                let is_action = msg.len() >= 8 && &msg.as_bytes()[..8] == ACTION_PREFIX.as_bytes();
+                let msg = if is_action {
+                    if msg.as_bytes()[msg.len() - 1] == 0x01 {
+                        &msg[8..msg.len() - 1]
+                    } else {
+                        &msg[8..]
+                    }
+                } else {
+                    msg
+                };
+                Cmd::PRIVMSG {
+                    target,
+                    msg: msg.to_owned(),
+                    is_notice,
+                    is_action,
+                }
+            }
+            MsgType::Cmd("JOIN") if params.len() == 1 => {
+                let chan = params[0];
+                Cmd::JOIN {
+                    chan: chan.to_owned(),
+                }
+            }
+            MsgType::Cmd("PART") if params.len() == 1 || params.len() == 2 => {
+                let mb_msg = if params.len() == 2 {
+                    Some(params[1].to_owned())
+                } else {
+                    None
+                };
+                Cmd::PART {
+                    chan: params[0].to_owned(),
+                    msg: mb_msg,
+                }
+            }
+            MsgType::Cmd("QUIT") if params.is_empty() || params.len() == 1 => {
+                let mb_msg = params.get(1).map(|s| (*s).to_owned());
+
+                Cmd::QUIT {
+                    msg: mb_msg,
+                    chans: Vec::new(),
+                }
+            }
+            MsgType::Cmd("NICK") if params.len() == 1 => {
+                let nick = params[0];
+                Cmd::NICK {
+                    nick: nick.to_owned(),
+                    chans: Vec::new(),
+                }
+            }
+            MsgType::Cmd("PING") if params.len() == 1 => Cmd::PING {
+                server: params[0].to_owned(),
+            },
+            MsgType::Cmd("PONG") if !params.is_empty() => Cmd::PONG {
+                server: params[0].to_owned(),
+            },
+            MsgType::Cmd("ERROR") if params.len() == 1 => Cmd::ERROR {
+                msg: params[0].to_owned(),
+            },
+            MsgType::Cmd("TOPIC") if params.len() == 2 => Cmd::TOPIC {
+                chan: params[0].to_owned(),
+                topic: params[1].to_owned(),
+            },
+            MsgType::Cmd("CAP") if params.len() == 3 => Cmd::CAP {
+                client: params[0].to_owned(),
+                subcommand: params[1].to_owned(),
+                params: params[2].split(' ').map(|s| s.to_owned()).collect(),
+            },
+            MsgType::Cmd("AUTHENTICATE") if params.len() == 1 => Cmd::AUTHENTICATE {
+                param: params[0].to_owned(),
+            },
+            MsgType::Num(n) => Cmd::Reply {
+                num: n,
+                params: params.into_iter().map(|s| s.to_owned()).collect(),
+            },
+            MsgType::Cmd(cmd) => Cmd::Other {
+                cmd: cmd.to_owned(),
+                params: params.into_iter().map(|s| s.to_owned()).collect(),
+            },
+        };
+
+        Msg { pfx, cmd }
+    };
+
+    buf.drain(0..crlf_idx + 2);
+    Some(ret)
+}
+
+fn parse_pfx(pfx: &[u8]) -> Pfx {
+    match find_byte(pfx, b'!') {
+        None => Pfx::Server(unsafe { str::from_utf8_unchecked(pfx).to_owned() }),
+        Some(idx) => Pfx::User {
+            nick: unsafe { str::from_utf8_unchecked(&pfx[0..idx]) }.to_owned(),
+            user: unsafe { str::from_utf8_unchecked(&pfx[idx + 1..]) }.to_owned(),
+        },
+    }
+}
+
+fn parse_reply_num(bs: &[u8]) -> Option<u16> {
+    fn is_num_ascii(b: u8) -> bool {
+        b >= b'0' && b <= b'9'
+    }
+
+    if bs.len() == 3 {
+        let n3 = bs[0];
+        let n2 = bs[1];
+        let n1 = bs[2];
+        if is_num_ascii(n3) && is_num_ascii(n2) && is_num_ascii(n1) {
+            return Some(
+                u16::from(n3 - b'0') * 100 + u16::from(n2 - b'0') * 10 + u16::from(n1 - b'0'),
+            );
+        }
+    }
+    None
+}
+
+fn parse_params(chrs: &str) -> Vec<&str> {
+    debug_assert_ne!(chrs.chars().nth(0), Some(' '));
+
+    let mut ret: Vec<&str> = Vec::new();
+
+    let mut slice_begins = 0;
+    for (char_idx, char) in chrs.char_indices() {
+        if char == ':' {
+            ret.push(unsafe { chrs.get_unchecked(char_idx + 1..chrs.len()) });
+            return ret;
+        } else if char == ' ' {
+            ret.push(unsafe { chrs.get_unchecked(slice_begins..char_idx) });
+            slice_begins = char_idx + 1;
+        }
+    }
+
+    if slice_begins != chrs.len() {
+        ret.push(unsafe { chrs.get_unchecked(slice_begins..chrs.len()) });
+    }
+
+    ret
+}
+
+pub fn find_byte(buf: &[u8], byte0: u8) -> Option<usize> {
+    for (byte_idx, byte) in buf.iter().enumerate() {
+        if *byte == byte0 {
+            return Some(byte_idx);
+        }
+    }
+    None
+}
+
+/// Nicks may have prefixes, indicating it is a operator, founder, or something else.
+///
+/// Channel Membership Prefixes: http://modern.ircdocs.horse/#channel-membership-prefixes
+///
+/// Returns the nick without prefix.
+pub fn drop_nick_prefix(nick: &str) -> &str {
+    static PREFIXES: [char; 5] = ['~', '&', '@', '%', '+'];
+
+    if PREFIXES.contains(&nick.chars().nth(0).unwrap()) {
+        &nick[1..]
+    } else {
+        nick
+    }
+}
+
+static ACTION_PREFIX: &str = "\x01ACTION ";
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::io::Write;
+
+    #[test]
+    fn test_parse_params() {
+        assert_eq!(parse_params("p1 p2 p3"), vec!["p1", "p2", "p3"]);
+        let v: Vec<&str> = vec![];
+        assert_eq!(parse_params(""), v);
+        assert_eq!(parse_params(":foo bar baz "), vec!["foo bar baz "]);
+        assert_eq!(parse_params(":"), vec![""]);
+    }
+
+    #[test]
+    fn test_privmsg_parsing() {
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            ":nick!~nick@unaffiliated/nick PRIVMSG tiny :a b c\r\n"
+        )
+        .unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: Some(Pfx::User {
+                    nick: "nick".to_owned(),
+                    user: "~nick@unaffiliated/nick".to_owned(),
+                }),
+                cmd: Cmd::PRIVMSG {
+                    target: MsgTarget::User("tiny".to_owned()),
+                    msg: "a b c".to_owned(),
+                    is_notice: false,
+                    is_action: false,
+                },
+            })
+        );
+        assert_eq!(buf.len(), 0);
+    }
+
+    #[test]
+    fn test_notice_parsing() {
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            ":barjavel.freenode.net NOTICE * :*** Looking up your hostname...\r\n"
+        )
+        .unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: Some(Pfx::Server("barjavel.freenode.net".to_owned())),
+                cmd: Cmd::PRIVMSG {
+                    target: MsgTarget::User("*".to_owned()),
+                    msg: "*** Looking up your hostname...".to_owned(),
+                    is_notice: true,
+                    is_action: false,
+                },
+            })
+        );
+    }
+
+    #[test]
+    fn test_numeric_parsing() {
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            ":barjavel.freenode.net 001 tiny :Welcome to the freenode Internet Relay Chat Network tiny\r\n"
+        ).unwrap();
+        write!(
+            &mut buf,
+            ":barjavel.freenode.net 002 tiny :Your host is barjavel.freenode.net[123.123.123.123/8001], \
+             running version ircd-seven-1.1.4\r\n"
+        ).unwrap();
+        write!(
+            &mut buf,
+            ":barjavel.freenode.net 004 tiny_test barjavel.freenode.net \
+             ircd-seven-1.1.4 DOQRSZaghilopswz \
+             CFILMPQSbcefgijklmnopqrstvz bkloveqjfI\r\n"
+        )
+        .unwrap();
+        write!(
+            &mut buf,
+            ":barjavel.freenode.net 005 tiny_test CHANTYPES=# EXCEPTS INVEX \
+             CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz CHANLIMIT=#:120 PREFIX=(ov)@+ \
+             MAXLIST=bqeI:100 MODES=4 NETWORK=freenode STATUSMSG=@+ CALLERID=g \
+             CASEMAPPING=rfc1459 :are supported by this server\r\n"
+        )
+        .unwrap();
+
+        let mut msgs = vec![];
+        while let Some(msg) = parse_irc_msg(&mut buf) {
+            msgs.push(msg);
+        }
+
+        assert_eq!(msgs.len(), 4);
+    }
+
+    #[test]
+    fn test_part_parsing() {
+        let mut buf = vec![];
+        write!(&mut buf, ":tiny!~tiny@123.123.123.123 PART #haskell\r\n").unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: Some(Pfx::User {
+                    nick: "tiny".to_owned(),
+                    user: "~tiny@123.123.123.123".to_owned(),
+                }),
+                cmd: Cmd::PART {
+                    chan: "#haskell".to_owned(),
+                    msg: None,
+                },
+            })
+        );
+    }
+
+    #[test]
+    fn test_join_parsing() {
+        let mut buf = vec![];
+        write!(&mut buf, ":tiny!~tiny@192.168.0.1 JOIN #haskell\r\n").unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: Some(Pfx::User {
+                    nick: "tiny".to_owned(),
+                    user: "~tiny@192.168.0.1".to_owned(),
+                }),
+                cmd: Cmd::JOIN {
+                    chan: "#haskell".to_owned(),
+                },
+            })
+        );
+        assert_eq!(buf.len(), 0);
+    }
+
+    // Example from https://tools.ietf.org/id/draft-oakley-irc-ctcp-01.html
+    #[test]
+    fn test_ctcp_action_parsing_1() {
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            ":dan!u@localhost PRIVMSG #ircv3 :\x01ACTION writes some specs!\x01\r\n"
+        )
+        .unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: Some(Pfx::User {
+                    nick: "dan".to_owned(),
+                    user: "u@localhost".to_owned(),
+                }),
+                cmd: Cmd::PRIVMSG {
+                    target: MsgTarget::Chan("#ircv3".to_owned()),
+                    msg: "writes some specs!".to_owned(),
+                    is_notice: false,
+                    is_action: true,
+                },
+            })
+        );
+        assert_eq!(buf.len(), 0);
+    }
+
+    #[test]
+    fn test_ctcp_action_parsing_2() {
+        // From https://modern.ircdocs.horse/ctcp.html:
+        //
+        // > The final <delim> MUST be sent, but parsers SHOULD accept incoming messages which lack
+        // > it (particularly for CTCP ACTION). This is due to how some software incorrectly
+        // > implements message splitting.
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            ":a!b@c PRIVMSG target :\x01ACTION msg contents\r\n"
+        )
+        .unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf).unwrap().cmd,
+            Cmd::PRIVMSG {
+                target: MsgTarget::User("target".to_owned()),
+                msg: "msg contents".to_owned(),
+                is_notice: false,
+                is_action: true,
+            }
+        );
+        assert_eq!(buf.len(), 0);
+
+        let mut buf = vec![];
+        write!(&mut buf, ":a!b@c PRIVMSG target :\x01ACTION \r\n").unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf).unwrap().cmd,
+            Cmd::PRIVMSG {
+                target: MsgTarget::User("target".to_owned()),
+                msg: "".to_owned(),
+                is_notice: false,
+                is_action: true,
+            }
+        );
+        assert_eq!(buf.len(), 0);
+
+        // This is a regression test: the slice [..8] takes the substring with only a part of one
+        // of the '’'s.
+        let mut buf = vec![];
+        write!(&mut buf, ":a!b@c PRIVMSG target :’’’’’’’\r\n").unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf).unwrap().cmd,
+            Cmd::PRIVMSG {
+                target: MsgTarget::User("target".to_owned()),
+                msg: "’’’’’’’".to_owned(),
+                is_notice: false,
+                is_action: false,
+            }
+        );
+        assert_eq!(buf.len(), 0);
+    }
+
+    #[test]
+    fn test_error_parsing() {
+        let mut buf = vec![];
+        write!(
+            &mut buf,
+            "ERROR :Closing Link: 212.252.143.51 (Excess Flood)\r\n"
+        )
+        .unwrap();
+        assert_eq!(
+            parse_irc_msg(&mut buf),
+            Some(Msg {
+                pfx: None,
+                cmd: Cmd::ERROR {
+                    msg: "Closing Link: 212.252.143.51 (Excess Flood)".to_owned(),
+                },
+            }),
+        );
+    }
+}
diff --git a/src/cmd.rs b/src/cmd.rs
index ab13057c..1cf1d595 100644
--- a/src/cmd.rs
+++ b/src/cmd.rs
@@ -1,25 +1,34 @@
-use super::Tiny;
+// I think borrowed boxing is necessary for objekt::clone_box to work
+#![allow(clippy::borrowed_box)]
+
 use crate::config;
-use crate::conn::Conn;
-use crate::notifier::Notifier;
-use crate::tui::{MsgSource, MsgTarget, Timestamp};
 use crate::utils;
-use mio::Poll;
-use std::error::Error;
+use libtiny_client::{Client, ServerInfo};
+use libtiny_ui::{MsgSource, MsgTarget, UI};
+use std::path::Path;
+
+pub(crate) struct CmdArgs<'a> {
+    pub args: &'a str,
+    pub config_path: &'a Path,
+    pub defaults: &'a config::Defaults,
+    pub ui: &'a Box<dyn UI>,
+    pub clients: &'a mut Vec<Client>,
+    pub src: MsgSource,
+}
 
-pub struct Cmd {
+pub(crate) struct Cmd {
     /// Command name. E.g. if this is `"cmd"`, `/cmd ...` will call this command.
-    pub name: &'static str,
+    pub(crate) name: &'static str,
 
     // Command help message. Shown in `/help`.
-    // pub help: &'static str,
+    // pub(crate) help: &'static str,
     /// Command function.
-    pub cmd_fn: for<'a, 'b> fn(&str, poll: &'b Poll, &'a mut Tiny<'b>, MsgSource),
+    pub(crate) cmd_fn: fn(CmdArgs),
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
-pub enum ParseCmdResult<'a> {
+pub(crate) enum ParseCmdResult<'a> {
     /// Command name parsing successful
     Ok {
         cmd: &'static Cmd,
@@ -34,7 +43,7 @@ pub enum ParseCmdResult<'a> {
     Unknown,
 }
 
-pub fn parse_cmd(cmd: &str) -> ParseCmdResult {
+pub(crate) fn parse_cmd(cmd: &str) -> ParseCmdResult {
     match cmd.split_whitespace().next() {
         None => ParseCmdResult::Unknown,
         Some(cmd_name) => {
@@ -69,23 +78,34 @@ pub fn parse_cmd(cmd: &str) -> ParseCmdResult {
     }
 }
 
+fn find_client_idx(clients: &[Client], serv_name: &str) -> Option<usize> {
+    for (client_idx, client) in clients.iter().enumerate() {
+        if client.get_serv_name() == serv_name {
+            return Some(client_idx);
+        }
+    }
+    None
+}
+
+fn find_client<'a>(clients: &'a mut Vec<Client>, serv_name: &str) -> Option<&'a mut Client> {
+    match find_client_idx(clients, serv_name) {
+        None => None,
+        Some(idx) => Some(&mut clients[idx]),
+    }
+}
+
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
-static CMDS: [&'static Cmd; 14] = [
+static CMDS: [&Cmd; 8] = [
     &AWAY_CMD,
-    &CLEAR_CMD,
     &CLOSE_CMD,
     &CONNECT_CMD,
-    &HELP_CMD,
-    &IGNORE_CMD,
     &JOIN_CMD,
     &ME_CMD,
     &MSG_CMD,
     &NAMES_CMD,
     &NICK_CMD,
-    &NOTIFY_CMD,
-    &RELOAD_CMD,
-    &SWITCH_CMD,
+    // &RELOAD_CMD,
 ];
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -95,50 +115,46 @@ static AWAY_CMD: Cmd = Cmd {
     cmd_fn: away,
 };
 
-fn away(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    let msg = if args.is_empty() { None } else { Some(args) };
-    if let Some(conn) = super::find_conn(&mut tiny.conns, src.serv_name()) {
-        conn.away(msg);
+fn away(args: CmdArgs) {
+    let msg = if args.args.is_empty() {
+        None
+    } else {
+        Some(args.args)
+    };
+    if let Some(client) = find_client(args.clients, args.src.serv_name()) {
+        client.away(msg);
     }
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
-static CLEAR_CMD: Cmd = Cmd {
-    name: "clear",
-    cmd_fn: clear,
-};
-
-fn clear(_: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    tiny.tui.clear(&src.to_target());
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
 static CLOSE_CMD: Cmd = Cmd {
     name: "close",
     cmd_fn: close,
 };
 
-fn close(_: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
+fn close(args: CmdArgs) {
+    let CmdArgs {
+        ui, clients, src, ..
+    } = args;
     match src {
-        MsgSource::Serv { ref serv_name } if serv_name == "mentions" => {
+        MsgSource::Serv { ref serv } if serv == "mentions" => {
             // ignore
         }
-        MsgSource::Serv { serv_name } => {
-            tiny.tui.close_server_tab(&serv_name);
-            let conn_idx = super::find_conn_idx(&tiny.conns, &serv_name).unwrap();
-            tiny.conns.remove(conn_idx);
+        MsgSource::Serv { serv } => {
+            ui.close_server_tab(&serv);
+            let client_idx = find_client_idx(&clients, &serv).unwrap();
+            // TODO: this probably won't close the connection?
+            let mut client = clients.remove(client_idx);
+            client.quit(None);
         }
-        MsgSource::Chan {
-            serv_name,
-            chan_name,
-        } => {
-            tiny.tui.close_chan_tab(&serv_name, &chan_name);
-            tiny.part(&serv_name, &chan_name);
+        MsgSource::Chan { serv, chan } => {
+            ui.close_chan_tab(&serv, &chan);
+            let client_idx = find_client_idx(&clients, &serv).unwrap();
+            clients[client_idx].part(&chan);
         }
-        MsgSource::User { serv_name, nick } => {
-            tiny.tui.close_user_tab(&serv_name, &nick);
+        MsgSource::User { serv, nick } => {
+            ui.close_user_tab(&serv, &nick);
         }
     }
 }
@@ -150,17 +166,25 @@ static CONNECT_CMD: Cmd = Cmd {
     cmd_fn: connect,
 };
 
-fn connect<'a, 'b>(args: &str, poll: &'b Poll, tiny: &'a mut Tiny<'b>, src: MsgSource) {
-    let words: Vec<&str> = args.split_whitespace().into_iter().collect();
+fn connect(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        defaults,
+        ui,
+        clients,
+        src,
+        ..
+    } = args;
+    let words: Vec<&str> = args.split_whitespace().collect();
 
     match words.len() {
-        0 => reconnect(tiny, src),
-        1 => connect_(words[0], None, poll, tiny),
-        2 => connect_(words[0], Some(words[1]), poll, tiny),
+        0 => reconnect(ui, clients, src),
+        1 => connect_(words[0], None, defaults, ui, clients),
+        2 => connect_(words[0], Some(words[1]), defaults, ui, clients),
         _ =>
         // wat
         {
-            tiny.tui.add_client_err_msg(
+            ui.add_client_err_msg(
                 "/connect usage: /connect <host>:<port> or /connect (to reconnect)",
                 &MsgTarget::CurrentTab,
             )
@@ -168,35 +192,25 @@ fn connect<'a, 'b>(args: &str, poll: &'b Poll, tiny: &'a mut Tiny<'b>, src: MsgS
     }
 }
 
-fn reconnect(tiny: &mut Tiny, src: MsgSource) {
-    tiny.tui.add_client_msg(
-        "Reconnecting...",
-        &MsgTarget::AllServTabs {
-            serv_name: src.serv_name(),
-        },
-    );
-    match super::find_conn(&mut tiny.conns, src.serv_name()) {
-        Some(conn) => match conn.reconnect(None) {
-            Ok(()) => {}
-            Err(err) => {
-                tiny.tui.add_err_msg(
-                    &super::reconnect_err_msg(&err),
-                    Timestamp::now(),
-                    &MsgTarget::AllServTabs {
-                        serv_name: conn.get_serv_name(),
-                    },
-                );
-            }
-        },
-        None => {
-            tiny.logger
-                .get_debug_logs()
-                .write_line(format_args!("Can't reconnect to {}", src.serv_name()));
-        }
+fn reconnect(ui: &Box<dyn UI>, clients: &mut Vec<Client>, src: MsgSource) {
+    if let Some(client) = find_client(clients, src.serv_name()) {
+        ui.add_client_msg(
+            "Reconnecting...",
+            &MsgTarget::AllServTabs {
+                serv: src.serv_name(),
+            },
+        );
+        client.reconnect(None);
     }
 }
 
-fn connect_<'a, 'b>(serv_addr: &str, pass: Option<&str>, poll: &'b Poll, tiny: &'a mut Tiny<'b>) {
+fn connect_(
+    serv_addr: &str,
+    pass: Option<&str>,
+    defaults: &config::Defaults,
+    ui: &Box<dyn UI>,
+    clients: &mut Vec<Client>,
+) {
     fn split_port(s: &str) -> Option<(&str, &str)> {
         s.find(':').map(|split| (&s[0..split], &s[split + 1..]))
     }
@@ -205,13 +219,12 @@ fn connect_<'a, 'b>(serv_addr: &str, pass: Option<&str>, poll: &'b Poll, tiny: &
     let (serv_name, serv_port) = {
         match split_port(serv_addr) {
             None => {
-                return tiny
-                    .tui
+                return ui
                     .add_client_err_msg("connect: Need a <host>:<port>", &MsgTarget::CurrentTab);
             }
             Some((serv_name, serv_port)) => match serv_port.parse::<u16>() {
                 Err(err) => {
-                    return tiny.tui.add_client_err_msg(
+                    return ui.add_client_err_msg(
                         &format!("connect: Can't parse port {}: {}", serv_port, err),
                         &MsgTarget::CurrentTab,
                     );
@@ -222,21 +235,9 @@ fn connect_<'a, 'b>(serv_addr: &str, pass: Option<&str>, poll: &'b Poll, tiny: &
     };
 
     // if we already connected to this server reconnect using new port
-    if let Some(conn) = super::find_conn(&mut tiny.conns, serv_name) {
-        tiny.tui
-            .add_client_msg("Connecting...", &MsgTarget::AllServTabs { serv_name });
-        match conn.reconnect(Some((serv_name, serv_port))) {
-            Ok(()) => {}
-            Err(err) => {
-                tiny.tui.add_err_msg(
-                    &super::reconnect_err_msg(&err),
-                    Timestamp::now(),
-                    &MsgTarget::AllServTabs {
-                        serv_name: conn.get_serv_name(),
-                    },
-                );
-            }
-        }
+    if let Some(client) = find_client(clients, serv_name) {
+        ui.add_client_msg("Connecting...", &MsgTarget::AllServTabs { serv: serv_name });
+        client.reconnect(Some(serv_port));
         return;
     }
 
@@ -244,78 +245,32 @@ fn connect_<'a, 'b>(serv_addr: &str, pass: Option<&str>, poll: &'b Poll, tiny: &
     // can't move the rest to an else branch because of borrowchk
 
     // otherwise create a new Conn, tab etc.
-    tiny.tui.new_server_tab(serv_name);
-    let msg_target = MsgTarget::Server { serv_name };
-    tiny.tui.add_client_msg("Connecting...", &msg_target);
+    ui.new_server_tab(serv_name);
+    let msg_target = MsgTarget::Server { serv: serv_name };
+    ui.add_client_msg("Connecting...", &msg_target);
 
-    let conn_ret = Conn::new(
-        config::Server {
+    let (client, rcv_ev) = Client::new(
+        ServerInfo {
             addr: serv_name.to_owned(),
             port: serv_port,
-            tls: tiny.defaults.tls,
-            hostname: tiny.defaults.hostname.clone(),
-            realname: tiny.defaults.realname.clone(),
+            tls: defaults.tls,
+            hostname: defaults.hostname.clone(),
+            realname: defaults.realname.clone(),
             pass: pass.map(str::to_owned),
-            nicks: tiny.defaults.nicks.clone(),
-            join: tiny.defaults.join.clone(),
+            nicks: defaults.nicks.clone(),
+            auto_join: defaults.join.clone(),
             nickserv_ident: None,
             sasl_auth: None,
         },
-        poll,
+        None, // tokio executor
     );
 
-    match conn_ret {
-        Ok(conn) => {
-            tiny.conns.push(conn);
-        }
-        Err(err) => {
-            tiny.tui
-                .add_err_msg(&super::connect_err_msg(&err), Timestamp::now(), &msg_target);
-        }
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static HELP_CMD: Cmd = Cmd {
-    name: "help",
-    cmd_fn: help,
-};
-
-fn help(_: &str, _: &Poll, _: &mut Tiny, _: MsgSource) {
-    // TODO
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static IGNORE_CMD: Cmd = Cmd {
-    name: "ignore",
-    cmd_fn: ignore,
-};
+    // Spawn UI task
+    let ui_clone = libtiny_ui::clone_box(&**ui);
+    let client_clone = client.clone();
+    tokio::runtime::current_thread::spawn(crate::conn::task(rcv_ev, ui_clone, client_clone));
 
-fn ignore(_: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    match src {
-        MsgSource::Serv { serv_name } => {
-            tiny.tui.toggle_ignore(&MsgTarget::AllServTabs {
-                serv_name: &serv_name,
-            });
-        }
-        MsgSource::Chan {
-            serv_name,
-            chan_name,
-        } => {
-            tiny.tui.toggle_ignore(&MsgTarget::Chan {
-                serv_name: &serv_name,
-                chan_name: &chan_name,
-            });
-        }
-        MsgSource::User { serv_name, nick } => {
-            tiny.tui.toggle_ignore(&MsgTarget::User {
-                serv_name: &serv_name,
-                nick: &nick,
-            });
-        }
-    }
+    clients.push(client);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -325,18 +280,25 @@ static JOIN_CMD: Cmd = Cmd {
     cmd_fn: join,
 };
 
-fn join(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
+fn join(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        ui,
+        clients,
+        src,
+        ..
+    } = args;
     let words = args.split_whitespace().collect::<Vec<_>>();
     if words.is_empty() {
-        return tiny.tui.add_client_err_msg(
+        return ui.add_client_err_msg(
             "/join usage: /join chan1[,chan2...]",
             &MsgTarget::CurrentTab,
         );
     }
 
-    match super::find_conn(&mut tiny.conns, src.serv_name()) {
-        Some(conn) => conn.join(&words),
-        None => tiny.tui.add_client_err_msg(
+    match find_client(clients, src.serv_name()) {
+        Some(client) => client.join(&words),
+        None => ui.add_client_err_msg(
             &format!("Can't JOIN: Not connected to server {}", src.serv_name()),
             &MsgTarget::CurrentTab,
         ),
@@ -350,13 +312,18 @@ static ME_CMD: Cmd = Cmd {
     cmd_fn: me,
 };
 
-fn me(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    if args.len() == 0 {
-        return tiny
-            .tui
-            .add_client_err_msg("/me usage: /me message", &MsgTarget::CurrentTab);
+fn me(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        ui,
+        clients,
+        src,
+        ..
+    } = args;
+    if args.is_empty() {
+        return ui.add_client_err_msg("/me usage: /me message", &MsgTarget::CurrentTab);
     }
-    tiny.send_msg(&src, args, true);
+    crate::ui::send_msg(&**ui, clients, &src, args.to_string(), true);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -384,10 +351,16 @@ fn split_msg_args(args: &str) -> Option<(&str, &str)> {
     target_msg
 }
 
-fn msg(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    let mut fail = || {
-        tiny.tui
-            .add_client_err_msg("/msg usage: /msg target message", &MsgTarget::CurrentTab);
+fn msg(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        ui,
+        clients,
+        src,
+        ..
+    } = args;
+    let fail = || {
+        ui.add_client_err_msg("/msg usage: /msg target message", &MsgTarget::CurrentTab);
     };
 
     let (target, msg) = match split_msg_args(args) {
@@ -401,19 +374,22 @@ fn msg(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
         }
     };
 
-    let src = if tiny.conns.iter().any(|conn| conn.get_serv_name() == target) {
+    let src = if clients
+        .iter()
+        .any(|client| client.get_serv_name() == target)
+    {
         MsgSource::Serv {
-            serv_name: target.to_owned(),
+            serv: target.to_owned(),
         }
     } else {
         let serv = src.serv_name();
         MsgSource::User {
-            serv_name: serv.to_owned(),
+            serv: serv.to_owned(),
             nick: target.to_owned(),
         }
     };
 
-    tiny.send_msg(&src, msg, false);
+    crate::ui::send_msg(&**ui, clients, &src, msg.to_owned(), false);
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -423,42 +399,41 @@ static NAMES_CMD: Cmd = Cmd {
     cmd_fn: names,
 };
 
-fn names(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
+fn names(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        ui,
+        src,
+        clients,
+        ..
+    } = args;
     let words: Vec<&str> = args.split_whitespace().collect();
 
-    if let MsgSource::Chan {
-        ref serv_name,
-        ref chan_name,
-    } = src
-    {
-        let nicks_vec = tiny
-            .tui
-            .get_nicks(serv_name, chan_name)
-            .map(|nicks| nicks.to_strings(""));
-        if let Some(nicks_vec) = nicks_vec {
-            let target = MsgTarget::Chan {
-                serv_name,
-                chan_name,
-            };
-            if words.is_empty() {
-                tiny.tui.add_client_msg(
-                    &format!("{} users: {}", nicks_vec.len(), nicks_vec.join(", ")),
-                    &target,
-                );
+    let client = match find_client(clients, src.serv_name()) {
+        None => {
+            return;
+        }
+        Some(client) => client,
+    };
+
+    if let MsgSource::Chan { ref serv, ref chan } = src {
+        let nicks_vec = client.get_chan_nicks(chan);
+        let target = MsgTarget::Chan { serv, chan };
+        if words.is_empty() {
+            ui.add_client_msg(
+                &format!("{} users: {}", nicks_vec.len(), nicks_vec.join(", ")),
+                &target,
+            );
+        } else {
+            let nick = words[0];
+            if nicks_vec.iter().any(|v| v == nick) {
+                ui.add_client_msg(&format!("{} is online", nick), &target);
             } else {
-                let nick = words[0];
-                if nicks_vec.iter().any(|v| v == nick) {
-                    tiny.tui
-                        .add_client_msg(&format!("{} is online", nick), &target);
-                } else {
-                    tiny.tui
-                        .add_client_msg(&format!("{} is not in the channel", nick), &target);
-                }
+                ui.add_client_msg(&format!("{} is not in the channel", nick), &target);
             }
         }
     } else {
-        tiny.tui
-            .add_client_err_msg("/names only supported in chan tabs", &MsgTarget::CurrentTab);
+        ui.add_client_err_msg("/names only supported in chan tabs", &MsgTarget::CurrentTab);
     }
 }
 
@@ -469,120 +444,48 @@ static NICK_CMD: Cmd = Cmd {
     cmd_fn: nick,
 };
 
-fn nick(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
+fn nick(args: CmdArgs) {
+    let CmdArgs {
+        args,
+        ui,
+        clients,
+        src,
+        ..
+    } = args;
     let words: Vec<&str> = args.split_whitespace().collect();
     if words.len() == 1 {
-        if let Some(conn) = super::find_conn(&mut tiny.conns, src.serv_name()) {
+        if let Some(client) = find_client(clients, src.serv_name()) {
             let new_nick = words[0];
-            conn.send_nick(new_nick);
+            client.nick(new_nick);
         }
     } else {
-        tiny.tui
-            .add_client_err_msg("/nick usage: /nick <nick>", &MsgTarget::CurrentTab);
+        ui.add_client_err_msg("/nick usage: /nick <nick>", &MsgTarget::CurrentTab);
     }
 }
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
+/*
 static RELOAD_CMD: Cmd = Cmd {
     name: "reload",
     cmd_fn: reload,
 };
 
-fn reload(_: &str, _: &Poll, tiny: &mut Tiny, _: MsgSource) {
-    match config::parse_config(&tiny.config_path) {
-        Ok(config::Config { colors, .. }) => tiny.tui.set_colors(colors),
+fn reload(args: CmdArgs) {
+    let CmdArgs {
+        config_path, ui, ..
+    } = args;
+    match config::parse_config(config_path) {
+        Ok(config::Config { colors, .. }) => ui.set_colors(colors),
         Err(err) => {
-            tiny.tui
-                .add_client_err_msg("Can't parse config file:", &MsgTarget::CurrentTab);
+            ui.add_client_err_msg("Can't parse config file:", &MsgTarget::CurrentTab);
             for line in err.description().lines() {
-                tiny.tui.add_client_err_msg(line, &MsgTarget::CurrentTab);
+                ui.add_client_err_msg(line, &MsgTarget::CurrentTab);
             }
         }
     }
 }
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static SWITCH_CMD: Cmd = Cmd {
-    name: "switch",
-    cmd_fn: switch,
-};
-
-fn switch(args: &str, _: &Poll, tiny: &mut Tiny, _: MsgSource) {
-    let words: Vec<&str> = args.split_whitespace().collect();
-    if words.len() != 1 {
-        return tiny
-            .tui
-            .add_client_err_msg("/switch usage: /switch <tab name>", &MsgTarget::CurrentTab);
-    }
-    tiny.tui.switch(words[0]);
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static NOTIFY_CMD: Cmd = Cmd {
-    name: "notify",
-    cmd_fn: notify,
-};
-
-fn notify(args: &str, _: &Poll, tiny: &mut Tiny, src: MsgSource) {
-    let words: Vec<&str> = args.split_whitespace().collect();
-
-    let mut show_usage = || {
-        tiny.tui.add_client_err_msg(
-            "/notify usage: /notify [off|mentions|messages]",
-            &MsgTarget::CurrentTab,
-        )
-    };
-
-    if words.is_empty() {
-        tiny.tui.show_notify_mode(&MsgTarget::CurrentTab);
-    } else if words.len() != 1 {
-        show_usage();
-    } else {
-        let notifier = match words[0] {
-            "off" => {
-                tiny.tui
-                    .add_client_notify_msg("Notifications turned off", &MsgTarget::CurrentTab);
-                Notifier::Off
-            }
-            "mentions" => {
-                tiny.tui.add_client_notify_msg(
-                    "Notifications enabled for mentions",
-                    &MsgTarget::CurrentTab,
-                );
-                Notifier::Mentions
-            }
-            "messages" => {
-                tiny.tui.add_client_notify_msg(
-                    "Notifications enabled for all messages",
-                    &MsgTarget::CurrentTab,
-                );
-                Notifier::Messages
-            }
-            _ => {
-                return show_usage();
-            }
-        };
-        // can't use `MsgSource::to_target` here, `Serv` case is different
-        let tab_target = match src {
-            MsgSource::Serv { ref serv_name } => MsgTarget::AllServTabs { serv_name },
-            MsgSource::Chan {
-                ref serv_name,
-                ref chan_name,
-            } => MsgTarget::Chan {
-                serv_name,
-                chan_name,
-            },
-            MsgSource::User {
-                ref serv_name,
-                ref nick,
-            } => MsgTarget::User { serv_name, nick },
-        };
-        tiny.tui.set_notifier(notifier, &tab_target);
-    }
-}
+*/
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/cmd_line_args.rs b/src/cmd_line_args.rs
index 99a60471..8d7eb41f 100644
--- a/src/cmd_line_args.rs
+++ b/src/cmd_line_args.rs
@@ -1,21 +1,19 @@
-use std::env::Args;
-use std::path::PathBuf;
+use std::{env::Args, path::PathBuf};
 
-pub struct CmdLineArgs {
+pub(crate) struct CmdLineArgs {
     /// Servers to connect to
-    pub servers: Vec<String>,
+    pub(crate) servers: Vec<String>,
 
     /// Path to config file
-    pub config_path: Option<PathBuf>,
+    pub(crate) config_path: Option<PathBuf>,
 }
 
-pub fn parse_cmd_line_args(args: Args) -> CmdLineArgs {
+pub(crate) fn parse_cmd_line_args(mut args: Args) -> CmdLineArgs {
     let mut parsed_args = CmdLineArgs {
         servers: Vec::new(),
         config_path: None,
     };
 
-    let mut args = args.into_iter();
     args.next(); // skip program name
 
     while let Some(arg) = args.next() {
diff --git a/src/config.rs b/src/config.rs
index e91b2155..39e54c73 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,84 +3,84 @@
 use dirs::home_dir;
 use serde::Deserialize;
 use serde_yaml;
-use std::fs::File;
-use std::io::Read;
-use std::io::Write;
-use std::path::Path;
-use std::path::PathBuf;
+use std::{
+    fs::File,
+    io::{Read, Write},
+    path::{Path, PathBuf},
+};
 
 #[derive(Clone, Deserialize, Debug, PartialEq, Eq)]
-pub struct SASLAuth {
-    pub username: String,
-    pub password: String,
+pub(crate) struct SASLAuth {
+    pub(crate) username: String,
+    pub(crate) password: String,
 }
 
 #[derive(Clone, Deserialize)]
-pub struct Server {
+pub(crate) struct Server {
     /// Address of the server
-    pub addr: String,
+    pub(crate) addr: String,
 
     /// Port of the server
-    pub port: u16,
+    pub(crate) port: u16,
 
     /// Use tls
     #[serde(default)]
-    pub tls: bool,
+    pub(crate) tls: bool,
 
     /// Server password (optional)
     #[serde(default)]
-    pub pass: Option<String>,
+    pub(crate) pass: Option<String>,
 
     /// Hostname to be used in connection registration
-    pub hostname: String,
+    pub(crate) hostname: String,
 
     /// Real name to be used in connection registration
-    pub realname: String,
+    pub(crate) realname: String,
 
     /// Nicks to try when connecting to this server. tiny tries these sequentially, and starts
     /// adding trailing underscores to the last one if none of the nicks are available.
-    pub nicks: Vec<String>,
+    pub(crate) nicks: Vec<String>,
 
     /// Channels to automatically join.
     #[serde(default)]
-    pub join: Vec<String>,
+    pub(crate) join: Vec<String>,
 
     /// NickServ identification password. Used on connecting to the server and nick change.
-    pub nickserv_ident: Option<String>,
+    pub(crate) nickserv_ident: Option<String>,
 
     /// Authenication method
     #[serde(rename = "sasl")]
-    pub sasl_auth: Option<SASLAuth>,
+    pub(crate) sasl_auth: Option<SASLAuth>,
 }
 
 /// Similar to `Server`, but used when connecting via the `/connect` command.
 #[derive(Clone, Deserialize)]
-pub struct Defaults {
-    pub nicks: Vec<String>,
-    pub hostname: String,
-    pub realname: String,
+pub(crate) struct Defaults {
+    pub(crate) nicks: Vec<String>,
+    pub(crate) hostname: String,
+    pub(crate) realname: String,
     #[serde(default)]
-    pub join: Vec<String>,
+    pub(crate) join: Vec<String>,
     #[serde(default)]
-    pub tls: bool,
+    pub(crate) tls: bool,
 }
 
 #[derive(Deserialize)]
-pub struct Config {
-    pub servers: Vec<Server>,
-    pub defaults: Defaults,
+pub(crate) struct Config {
+    pub(crate) servers: Vec<Server>,
+    pub(crate) defaults: Defaults,
     #[serde(default)]
-    pub colors: Colors,
-    pub log_dir: String,
+    pub(crate) colors: libtiny_tui::Colors,
+    pub(crate) log_dir: Option<PathBuf>,
 }
 
-pub fn get_default_config_path() -> PathBuf {
+pub(crate) fn get_default_config_path() -> PathBuf {
     let mut config_path = home_dir().unwrap();
     config_path.push(".tinyrc.yml");
     config_path
 }
 
-pub fn parse_config(config_path: &Path) -> Result<Config, serde_yaml::Error> {
+pub(crate) fn parse_config(config_path: &Path) -> Result<Config, serde_yaml::Error> {
     let contents = {
         let mut str = String::new();
         let mut file = File::open(config_path).unwrap();
@@ -95,7 +95,7 @@ fn parse_config_str(contents: &str) -> Result<Config, serde_yaml::Error> {
     serde_yaml::from_str(&contents)
 }
 
-pub fn generate_default_config(config_path: &Path) {
+pub(crate) fn generate_default_config(config_path: &Path) {
     if let Some(parent) = config_path.parent() {
         let _ = ::std::fs::create_dir_all(parent);
     }
@@ -119,244 +119,6 @@ fn get_default_config_yaml() -> String {
     )
 }
 
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Colors
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-use serde::de::{self, Deserializer, MapAccess, Visitor};
-pub use termbox_simple::*;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct Style {
-    /// Termbox fg.
-    pub fg: u16,
-
-    /// Termbox bg.
-    pub bg: u16,
-}
-
-#[derive(Debug, Deserialize)]
-#[serde(default)]
-pub struct Colors {
-    pub nick: Vec<u8>,
-    pub clear: Style,
-    pub user_msg: Style,
-    pub err_msg: Style,
-    pub topic: Style,
-    pub cursor: Style,
-    pub join: Style,
-    pub part: Style,
-    pub nick_change: Style,
-    pub faded: Style,
-    pub exit_dialogue: Style,
-    pub highlight: Style,
-    pub completion: Style,
-    pub timestamp: Style,
-    pub tab_active: Style,
-    pub tab_normal: Style,
-    pub tab_new_msg: Style,
-    pub tab_highlight: Style,
-}
-
-impl Default for Colors {
-    fn default() -> Self {
-        Colors {
-            nick: vec![1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14],
-            clear: Style {
-                fg: TB_DEFAULT,
-                bg: TB_DEFAULT,
-            },
-            user_msg: Style {
-                fg: 0,
-                bg: TB_DEFAULT,
-            },
-            err_msg: Style { fg: TB_BOLD, bg: 1 },
-            topic: Style {
-                fg: 14 | TB_BOLD,
-                bg: TB_DEFAULT,
-            },
-            cursor: Style {
-                fg: 0,
-                bg: TB_DEFAULT,
-            },
-            join: Style {
-                fg: 10 | TB_BOLD,
-                bg: TB_DEFAULT,
-            },
-            part: Style {
-                fg: 1 | TB_BOLD,
-                bg: TB_DEFAULT,
-            },
-            nick_change: Style {
-                fg: 10 | TB_BOLD,
-                bg: TB_DEFAULT,
-            },
-            faded: Style {
-                fg: 242,
-                bg: TB_DEFAULT,
-            },
-            exit_dialogue: Style {
-                fg: TB_DEFAULT,
-                bg: 4,
-            },
-            highlight: Style {
-                fg: 9 | TB_BOLD,
-                bg: TB_DEFAULT,
-            },
-            completion: Style {
-                fg: 84,
-                bg: TB_DEFAULT,
-            },
-            timestamp: Style {
-                fg: 242,
-                bg: TB_DEFAULT,
-            },
-            tab_active: Style { fg: TB_BOLD, bg: 0 },
-            tab_normal: Style { fg: 8, bg: 0 },
-            tab_new_msg: Style { fg: 5, bg: 0 },
-            tab_highlight: Style {
-                fg: 9 | TB_BOLD,
-                bg: 0,
-            },
-        }
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Color parsing
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-// Color names are taken from https://en.wikipedia.org/wiki/List_of_software_palettes
-const COLORS: [(&str, u16); 17] = [
-    ("default", TB_DEFAULT), // Default fg/bg color of the terminal
-    ("black", 0),
-    ("maroon", 1),
-    ("green", 2),
-    ("olive", 3),
-    ("navy", 4),
-    ("purple", 5),
-    ("teal", 6),
-    ("silver", 7),
-    ("gray", 8),
-    ("red", 9),
-    ("lime", 10),
-    ("yellow", 11),
-    ("blue", 12),
-    ("magenta", 13),
-    ("cyan", 14),
-    ("white", 15),
-];
-
-const ATTRS: [(&str, u16); 2] = [("bold", TB_BOLD), ("underline", TB_UNDERLINE)];
-
-fn parse_color(val: String) -> Option<u16> {
-    for &(name, color) in &COLORS {
-        if val == name {
-            return Some(color);
-        }
-    }
-
-    // If color name doesn't match try get a number
-    val.parse().ok()
-}
-
-fn parse_attr(val: String) -> u16 {
-    for &(name, attr) in &ATTRS {
-        if name == val {
-            return attr;
-        }
-    }
-    0
-}
-
-impl<'de> Deserialize<'de> for Style {
-    fn deserialize<D>(d: D) -> Result<Self, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        #[derive(Deserialize)]
-        #[serde(field_identifier, rename_all = "lowercase")]
-        enum Field {
-            Fg,
-            Bg,
-            Attrs,
-        }
-
-        use std::fmt;
-
-        struct StyleVisitor;
-        impl<'de> Visitor<'de> for StyleVisitor {
-            type Value = Style;
-
-            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-                let colors = COLORS
-                    .iter()
-                    .map(|&(name, _)| name)
-                    .collect::<Vec<&str>>()
-                    .join(", ");
-                let attrs = ATTRS
-                    .iter()
-                    .map(|&(name, _)| name)
-                    .collect::<Vec<&str>>()
-                    .join(", ");
-
-                writeln!(
-                    formatter,
-                    "fg: 0-255 or color name\n\
-                     bg: 0-255 or color name\n\
-                     attrs: [{}]\n\n\
-                     color names: {}",
-                    attrs, colors
-                )
-            }
-
-            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
-            where
-                M: MapAccess<'de>,
-            {
-                let mut fg: Option<u16> = None;
-                let mut bg: Option<u16> = None;
-                let mut attr: u16 = 0;
-
-                while let Some(key) = map.next_key()? {
-                    match key {
-                        Field::Fg => {
-                            let color = parse_color(map.next_value()?).ok_or_else(|| {
-                                de::Error::invalid_value(de::Unexpected::UnitVariant, &self)
-                            })?;
-
-                            fg = Some(color);
-                        }
-
-                        Field::Bg => {
-                            let color = parse_color(map.next_value()?).ok_or_else(|| {
-                                de::Error::invalid_value(de::Unexpected::UnitVariant, &self)
-                            })?;
-
-                            bg = Some(color);
-                        }
-
-                        Field::Attrs => {
-                            let attrs: Vec<String> = map.next_value()?;
-                            attr = attrs
-                                .into_iter()
-                                .map(parse_attr)
-                                .fold(0, |style, a| style | a);
-                        }
-                    }
-                }
-
-                let fg = fg.ok_or_else(|| de::Error::missing_field("fg"))?;
-                let bg = bg.ok_or_else(|| de::Error::missing_field("bg"))?;
-
-                Ok(Style { fg: fg | attr, bg })
-            }
-        }
-
-        d.deserialize_map(StyleVisitor)
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -367,7 +129,7 @@ mod tests {
         match serde_yaml::from_str(&get_default_config_yaml()) {
             Err(yaml_err) => {
                 println!("{}", yaml_err);
-                assert!(false);
+                panic!();
             }
             Ok(Config { servers, .. }) => {
                 assert_eq!(
diff --git a/src/conn.rs b/src/conn.rs
index b8d4278d..f5a40390 100644
--- a/src/conn.rs
+++ b/src/conn.rs
@@ -1,830 +1,495 @@
-extern crate base64;
-
-use mio::Poll;
-use mio::Token;
-use std::io::Write;
-use std::result;
-use std::str;
-
-use crate::config;
-use crate::logger::LogFile;
-use crate::logger::Logger;
-use crate::stream::{Stream, StreamErr};
-use crate::utils;
-use crate::wire;
-use crate::wire::{Cmd, Msg, Pfx};
-
-pub struct Conn<'poll> {
-    serv_addr: String,
-    serv_port: u16,
-    tls: bool,
-    hostname: String,
-    realname: String,
-
-    /// Server password
-    pass: Option<String>,
-
-    nicks: Vec<String>,
-
-    /// Always in range of `nicks`
-    current_nick_idx: usize,
-
-    /// Channels to auto-join. Every channel we join will be added here to be able to re-join
-    /// automatically on reconnect and channels we leave will be removed.
-    ///
-    /// Technically a set but we want to join channels in the order given by the user, so using
-    /// `Vec` here.
-    auto_join: Vec<String>,
-
-    /// Nickserv password. Sent to NickServ on connecting to the server and nick change, before
-    /// join commands.
-    nickserv_ident: Option<String>,
-
-    /// Away reason if away mode is on. `None` otherwise.
-    away_status: Option<String>,
-
-    /// servername to be used in PING messages. Read from 002 RPL_YOURHOST.
-    /// `None` until 002.
-    servername: Option<String>,
-
-    /// Our usermask given by the server. Currently only parsed after a JOIN,
-    /// reply 396.
-    ///
-    /// Note that RPL_USERHOST (302) does not take cloaks into account, so we
-    /// don't parse USERHOST responses to set this field.
-    usermask: Option<String>,
-
-    poll: &'poll Poll,
-
-    status: ConnStatus<'poll>,
-
-    /// Incoming message buffer
-    in_buf: Vec<u8>,
-
-    sasl_auth: Option<config::SASLAuth>,
-
-    /// Do we have a nick yet? Try another nick on ERR_NICKNAMEINUSE (433) until we've got a nick.
-    nick_accepted: bool,
-}
-
-pub type ConnErr = StreamErr;
-
-/// How many ticks to wait before sending a ping to the server.
-const PING_TICKS: u8 = 60;
-/// How many ticks to wait after sending a ping to the server to consider a
-/// disconnect.
-const PONG_TICKS: u8 = 60;
-/// How many ticks to wait after a disconnect or a socket error.
-pub const RECONNECT_TICKS: u8 = 30;
-
-enum ConnStatus<'poll> {
-    PingPong {
-        /// Ticks passed since last time we've heard from the server. Reset on
-        /// each message. After `PING_TICKS` ticks we send a PING message and
-        /// move to `WaitPong` state.
-        ticks_passed: u8,
-        stream: Stream<'poll>,
-    },
-    WaitPong {
-        /// Ticks passed since we sent a PING to the server. After a message
-        /// move to `PingPong` state. On timeout we reset the connection.
-        ticks_passed: u8,
-        stream: Stream<'poll>,
-    },
-    Disconnected {
-        ticks_passed: u8,
-    },
-}
-
-macro_rules! update_status {
-    ($self:ident, $v:ident, $code:expr) => {{
-        // temporarily putting `Disconnected` to `self.status`
-        let $v = ::std::mem::replace(
-            &mut $self.status,
-            ConnStatus::Disconnected { ticks_passed: 0 },
-        );
-        let new_status = $code;
-        $self.status = new_status;
-    }};
-}
-
-impl<'poll> ConnStatus<'poll> {
-    fn get_stream(&self) -> Option<&Stream<'poll>> {
-        use self::ConnStatus::*;
-        match *self {
-            PingPong { ref stream, .. } | WaitPong { ref stream, .. } => Some(stream),
-            Disconnected { .. } => None,
-        }
-    }
-
-    fn get_stream_mut(&mut self) -> Option<&mut Stream<'poll>> {
-        use self::ConnStatus::*;
-        match *self {
-            PingPong { ref mut stream, .. } | WaitPong { ref mut stream, .. } => Some(stream),
-            Disconnected { .. } => None,
-        }
-    }
-}
-
-pub type Result<T> = result::Result<T, StreamErr>;
-
-#[derive(Debug)]
-pub enum ConnEv {
-    /// Connected to the server + registered
-    Connected,
-    ///
-    Disconnected,
-    /// Hack to return the main loop that the Conn wants reconnect()
-    WantReconnect,
-    /// Network error happened
-    Err(StreamErr),
-    /// An incoming message
-    Msg(Msg),
-    /// Nick changed
-    NickChange(String),
-}
-
-fn introduce<W: Write>(
-    stream: &mut W,
-    pass: Option<&str>,
-    hostname: &str,
-    realname: &str,
-    nick: &str,
+#![allow(clippy::cognitive_complexity)]
+#![allow(clippy::unneeded_field_pattern)]
+
+//! IRC event handling
+
+use futures_util::stream::StreamExt;
+use libtiny_client::Client;
+use libtiny_ui::{MsgTarget, TabStyle, UI};
+use libtiny_wire as wire;
+use std::error::Error;
+use tokio::sync::mpsc;
+
+pub(crate) async fn task(
+    mut rcv_ev: mpsc::Receiver<libtiny_client::Event>,
+    ui: Box<dyn UI>,
+    client: Client,
 ) {
-    if let Some(pass) = pass {
-        wire::pass(stream, pass).unwrap();
+    while let Some(ev) = rcv_ev.next().await {
+        if handle_conn_ev(&*ui, &client, ev) {
+            return;
+        }
+        ui.draw();
     }
-    wire::nick(stream, nick).unwrap();
-    wire::user(stream, hostname, realname).unwrap();
 }
 
-impl<'poll> Conn<'poll> {
-    pub fn new(server: config::Server, poll: &'poll Poll) -> Result<Conn<'poll>> {
-        let mut stream =
-            Stream::new(poll, &server.addr, server.port, server.tls).map_err(StreamErr::from)?;
-
-        if server.sasl_auth.is_some() {
-            // Will introduce self after getting a response to this LS command.
-            // This is to avoid getting stuck during nick registration. See the
-            // discussion in #91.
-            wire::cap_ls(&mut stream).unwrap();
-        } else {
-            introduce(
-                &mut stream,
-                server.pass.as_ref().map(String::as_str),
-                &server.hostname,
-                &server.realname,
-                &server.nicks[0],
+fn handle_conn_ev(ui: &dyn UI, client: &Client, ev: libtiny_client::Event) -> bool {
+    use libtiny_client::Event::*;
+    match ev {
+        Connecting => {
+            ui.add_client_msg(
+                "Connecting...",
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
             );
         }
-
-        Ok(Conn {
-            serv_addr: server.addr,
-            serv_port: server.port,
-            tls: server.tls,
-            hostname: server.hostname,
-            realname: server.realname,
-            pass: server.pass,
-            nicks: server.nicks,
-            current_nick_idx: 0,
-            auto_join: server.join,
-            nickserv_ident: server.nickserv_ident,
-            away_status: None,
-            servername: None,
-            usermask: None,
-            poll,
-            status: ConnStatus::PingPong {
-                ticks_passed: 0,
-                stream,
-            },
-            in_buf: vec![],
-            sasl_auth: server.sasl_auth,
-            nick_accepted: false,
-        })
-    }
-
-    pub fn reconnect(&mut self, new_serv: Option<(&str, u16)>) -> Result<()> {
-        // drop existing connection first
-        let old_stream = ::std::mem::replace(
-            &mut self.status,
-            ConnStatus::Disconnected { ticks_passed: 0 },
-        );
-        drop(old_stream);
-
-        self.nick_accepted = false;
-
-        if let Some((new_name, new_port)) = new_serv {
-            self.serv_addr = new_name.to_owned();
-            self.serv_port = new_port;
+        Connected => {
+            ui.add_msg(
+                "Connected.",
+                time::now(),
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
+            );
         }
-        match Stream::new(self.poll, &self.serv_addr, self.serv_port, self.tls) {
-            Err(err) => Err(err),
-            Ok(mut stream) => {
-                if self.sasl_auth.is_some() {
-                    wire::cap_ls(&mut stream).unwrap();
-                } else {
-                    introduce(
-                        &mut stream,
-                        self.pass.as_ref().map(String::as_str),
-                        &self.hostname,
-                        &self.realname,
-                        self.get_nick(),
-                    );
-                }
-                self.status = ConnStatus::PingPong {
-                    ticks_passed: 0,
-                    stream,
-                };
-                self.current_nick_idx = 0;
-                Ok(())
-            }
+        Disconnected => {
+            let serv = client.get_serv_name();
+            ui.add_err_msg(
+                &format!(
+                    "Disconnected. Will try to reconnect in {} seconds.",
+                    libtiny_client::RECONNECT_SECS
+                ),
+                time::now(),
+                &MsgTarget::AllServTabs { serv },
+            );
+            ui.clear_nicks(serv);
         }
-    }
-
-    pub fn get_conn_tok(&self) -> Option<Token> {
-        self.status.get_stream().map(|s| s.get_tok())
-    }
-
-    pub fn get_serv_name(&self) -> &str {
-        &self.serv_addr
-    }
-
-    pub fn get_nick(&self) -> &str {
-        &self.nicks[self.current_nick_idx]
-    }
-
-    pub fn is_nick_accepted(&self) -> bool {
-        self.nick_accepted
-    }
-
-    /// Update the current nick state. Only do this after a new nick has given/accepted by the
-    /// server.
-    fn set_nick(&mut self, nick: &str) {
-        if let Some(nick_idx) = self.nicks.iter().position(|n| n == nick) {
-            self.current_nick_idx = nick_idx;
-        } else {
-            self.nicks.push(nick.to_owned());
-            self.current_nick_idx = self.nicks.len() - 1;
+        IoErr(err) => {
+            ui.add_err_msg(
+                &format!(
+                    "Connection error: {}. Will try to reconnect in {} seconds.",
+                    err.description(),
+                    libtiny_client::RECONNECT_SECS
+                ),
+                time::now(),
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
+            );
         }
-    }
-
-    fn next_nick(&mut self) {
-        if self.current_nick_idx + 1 == self.nicks.len() {
-            let mut new_nick = self.nicks.last().unwrap().to_string();
-            new_nick.push('_');
-            self.status.get_stream_mut().map(|stream| {
-                wire::nick(stream, &new_nick).unwrap();
-            });
-            self.nicks.push(new_nick);
+        TlsErr(err) => {
+            ui.add_err_msg(
+                &format!(
+                    "TLS error: {}. Will try to reconnect in {} seconds.",
+                    err.description(),
+                    libtiny_client::RECONNECT_SECS
+                ),
+                time::now(),
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
+            );
         }
-        self.current_nick_idx += 1;
-    }
-}
-
-impl<'poll> Conn<'poll> {
-    fn plain_sasl_authenticate(&mut self) {
-        if let (Some(stream), Some(auth)) = (self.status.get_stream_mut(), self.sasl_auth.as_ref())
-        {
-            let msg = format!(
-                "{}\x00{}\x00{}",
-                auth.username, auth.username, auth.password
+        CantResolveAddr => {
+            ui.add_err_msg(
+                "Can't resolve address",
+                time::now(),
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
             );
-            wire::authenticate(stream, &base64::encode(&msg)).unwrap();
+        }
+        NickChange(new_nick) => {
+            ui.set_nick(client.get_serv_name(), &new_nick);
+        }
+        Msg(msg) => {
+            handle_irc_msg(ui, client, msg);
+        }
+        Closed => {
+            return true;
         }
     }
+    false
+}
 
-    fn end_capability_negotiation(&mut self) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::cap_end(stream).unwrap();
-        });
-    }
-
-    pub fn enter_disconnect_state(&mut self) {
-        self.status = ConnStatus::Disconnected { ticks_passed: 0 };
-        self.nick_accepted = false;
-    }
+fn handle_irc_msg(ui: &dyn UI, client: &Client, msg: wire::Msg) {
+    use wire::Cmd::*;
+    use wire::Pfx::*;
+
+    let wire::Msg { pfx, cmd } = msg;
+    let ts = time::now();
+    match cmd {
+        PRIVMSG {
+            target,
+            msg,
+            is_notice,
+            is_action,
+        } => {
+            let pfx = match pfx {
+                Some(pfx) => pfx,
+                None => {
+                    // TODO: log this?
+                    return;
+                }
+            };
 
-    ////////////////////////////////////////////////////////////////////////////
-    // Tick handling
+            // sender to be shown in the UI
+            let origin = match pfx {
+                Server(_) => client.get_serv_name(),
+                User { ref nick, .. } => nick,
+            };
 
-    pub fn tick(&mut self, evs: &mut Vec<ConnEv>, mut debug_out: LogFile) {
-        update_status!(
-            self,
-            status,
-            match status {
-                ConnStatus::PingPong {
-                    mut stream,
-                    ticks_passed,
-                } => {
-                    let ticks = ticks_passed + 1;
-                    if ticks == PING_TICKS {
-                        match self.servername {
-                            None => {
-                                debug_out.write_line(format_args!(
-                                    "{}: Can't send PING, servername unknown",
-                                    self.serv_addr
-                                ));
-                            }
-                            Some(ref host_) => {
-                                debug_out.write_line(format_args!(
-                                    "{}: Ping timeout, sending PING",
-                                    self.serv_addr
-                                ));
-                                wire::ping(&mut stream, host_).unwrap();
-                            }
-                        }
-                        ConnStatus::WaitPong {
-                            stream,
-                            ticks_passed: 0,
-                        }
+            match target {
+                wire::MsgTarget::Chan(chan) => {
+                    let ui_msg_target = MsgTarget::Chan {
+                        serv: client.get_serv_name(),
+                        chan: &chan,
+                    };
+                    // highlight the message if it mentions us
+                    if msg.find(&client.get_nick()).is_some() {
+                        ui.add_privmsg(origin, &msg, ts, &ui_msg_target, true, is_action);
+                        ui.set_tab_style(TabStyle::Highlight, &ui_msg_target);
+                        let mentions_target = MsgTarget::Server { serv: "mentions" };
+                        ui.add_msg(
+                            &format!("{} in {}:{}: {}", origin, client.get_serv_name(), chan, msg),
+                            ts,
+                            &mentions_target,
+                        );
+                        ui.set_tab_style(TabStyle::Highlight, &mentions_target);
                     } else {
-                        ConnStatus::PingPong {
-                            stream,
-                            ticks_passed: ticks,
-                        }
+                        ui.add_privmsg(origin, &msg, ts, &ui_msg_target, false, is_action);
+                        ui.set_tab_style(TabStyle::NewMsg, &ui_msg_target);
                     }
                 }
-                ConnStatus::WaitPong {
-                    stream,
-                    ticks_passed,
-                } => {
-                    let ticks = ticks_passed + 1;
-                    if ticks == PONG_TICKS {
-                        evs.push(ConnEv::Disconnected);
-                        self.nick_accepted = false;
-                        ConnStatus::Disconnected { ticks_passed: 0 }
-                    } else {
-                        ConnStatus::WaitPong {
-                            stream,
-                            ticks_passed: ticks,
+                wire::MsgTarget::User(target) => {
+                    let serv = client.get_serv_name();
+                    let msg_target = {
+                        match pfx {
+                            Server(_) => MsgTarget::Server { serv },
+                            User { ref nick, .. } => {
+                                // show NOTICE messages in server tabs if we don't have a tab
+                                // for the sender already (see #21)
+                                if is_notice && !ui.user_tab_exists(serv, nick) {
+                                    MsgTarget::Server { serv }
+                                } else {
+                                    MsgTarget::User { serv, nick }
+                                }
+                            }
                         }
-                    }
-                }
-                ConnStatus::Disconnected { ticks_passed } => {
-                    let ticks = ticks_passed + 1;
-                    if ticks_passed + 1 == RECONNECT_TICKS {
-                        // *sigh* it's slightly annoying that we can't reconnect here, we need to
-                        // update the event loop
-                        evs.push(ConnEv::WantReconnect);
-                        self.current_nick_idx = 0;
-                    }
-                    ConnStatus::Disconnected {
-                        ticks_passed: ticks,
+                    };
+                    ui.add_privmsg(origin, &msg, ts, &msg_target, false, is_action);
+                    if target == client.get_nick() {
+                        ui.set_tab_style(TabStyle::Highlight, &msg_target);
+                    } else {
+                        // not sure if this case can happen
+                        ui.set_tab_style(TabStyle::NewMsg, &msg_target);
                     }
                 }
             }
-        );
-    }
+        }
 
-    fn reset_ticks(&mut self) {
-        update_status!(
-            self,
-            status,
-            match status {
-                ConnStatus::PingPong { stream, .. } => ConnStatus::PingPong {
-                    ticks_passed: 0,
-                    stream
-                },
-                ConnStatus::WaitPong { stream, .. } =>
-                // no bug: we heard something from the server, whether it was a pong or not
-                // doesn't matter that much, connectivity is fine.
-                {
-                    ConnStatus::PingPong {
-                        ticks_passed: 0,
-                        stream
-                    }
+        JOIN { chan } => {
+            let nick = match pfx {
+                Some(User { nick, .. }) => nick,
+                _ => {
+                    // TODO: log this?
+                    return;
                 }
-                ConnStatus::Disconnected { .. } => status,
-            }
-        );
-    }
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Sending messages
-
-    /// Send a nick message. Does not mean we will be successfully changing the nick, the new nick
-    /// may be in use or for some other reason server may reject the request. Expect ERR_NICKINUSE
-    /// or NICK message in response.
-    pub fn send_nick(&mut self, nick: &str) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::nick(stream, nick).unwrap();
-        });
-    }
-
-    fn nickserv_ident(&mut self) {
-        // FIXME: privmsg method inlined below to work around a borrowchk error
-        if let Some(ref pwd) = self.nickserv_ident {
-            self.status.get_stream_mut().map(|stream| {
-                wire::privmsg(stream, "NickServ", &format!("identify {}", pwd)).unwrap();
-            });
-        }
-    }
+            };
 
-    /// `extra_len`: Size (in bytes) for a prefix/suffix etc. that'll be added to each line.
-    /// Strings returned by the iterator will have enough room for that.
-    pub fn split_privmsg<'a>(&self, extra_len: i32, msg: &'a str) -> utils::SplitIterator<'a> {
-        // Max msg len calculation adapted from hexchat
-        // (src/common/outbound.c:split_up_text)
-        let mut max: i32 = 512; // RFC 2812
-        max -= 3; // :, !, @
-        max -= 13; // " PRIVMSG ", " ", :, \r, \n
-        max -= self.get_nick().len() as i32;
-        max -= extra_len;
-        match self.usermask {
-            None => {
-                max -= 9; // max username
-                max -= 64; // max possible hostname (63) + '@'
-                           // NOTE(osa): I think hexchat has an error here, it
-                           // uses 65
-            }
-            Some(ref usermask) => {
-                max -= usermask.len() as i32;
+            let serv = client.get_serv_name();
+            if nick == client.get_nick() {
+                ui.new_chan_tab(serv, &chan);
+            } else {
+                let nick = wire::drop_nick_prefix(&nick);
+                let ts = Some(time::now());
+                ui.add_nick(nick, ts, &MsgTarget::Chan { serv, chan: &chan });
+                // Also update the private message tab if it exists
+                // Nothing will be shown if the user already known to be online by the tab
+                if ui.user_tab_exists(serv, nick) {
+                    ui.add_nick(nick, ts, &MsgTarget::User { serv, nick });
+                }
             }
         }
 
-        assert!(max > 0);
-
-        utils::split_iterator(msg, max as usize)
-    }
-
-    // FIXME: This crashes with an assertion error when the message is too long
-    // to fit into 512 bytes. Need to make sure `split_privmsg` is called before
-    // this.
-    pub fn privmsg(&mut self, target: &str, msg: &str) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::privmsg(stream, target, msg).unwrap();
-        });
-    }
-
-    pub fn ctcp_action(&mut self, target: &str, msg: &str) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::ctcp_action(stream, target, msg).unwrap();
-        });
-    }
-
-    pub fn join(&mut self, chans: &[&str]) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::join(stream, chans).unwrap();
-        });
-        // the channel will be added to auto-join list on successful join (i.e.
-        // after RPL_TOPIC)
-    }
-
-    pub fn part(&mut self, chan: &str) {
-        self.status.get_stream_mut().map(|stream| {
-            wire::part(stream, chan).unwrap();
-        });
-        self.auto_join.drain_filter(|chan_| chan_ == chan);
-    }
-
-    pub fn away(&mut self, msg: Option<&str>) {
-        self.away_status = msg.map(|s| s.to_string());
-        self.status.get_stream_mut().map(|stream| {
-            wire::away(stream, msg).unwrap();
-        });
-    }
-
-    pub fn raw_msg(&mut self, msg: &str) {
-        self.status.get_stream_mut().map(|stream| {
-            write!(stream, "{}\r\n", msg).unwrap();
-        });
-    }
-
-    ////////////////////////////////////////////////////////////////////////////
-    // Sending messages
-
-    pub fn write_ready(&mut self, evs: &mut Vec<ConnEv>) {
-        if let Some(stream) = self.status.get_stream_mut() {
-            match stream.write_ready() {
-                Err(err) => {
-                    if !err.is_would_block() {
-                        evs.push(ConnEv::Err(err));
-                    }
+        PART { chan, .. } => {
+            let nick = match pfx {
+                Some(User { nick, .. }) => nick,
+                _ => {
+                    // TODO: log this?
+                    return;
                 }
-                Ok(()) => {}
+            };
+            if nick != client.get_nick() {
+                ui.remove_nick(
+                    &nick,
+                    Some(time::now()),
+                    &MsgTarget::Chan {
+                        serv: client.get_serv_name(),
+                        chan: &chan,
+                    },
+                );
             }
         }
-    }
 
-    ////////////////////////////////////////////////////////////////////////////
-    // Receiving messages
-
-    pub fn read_ready(&mut self, evs: &mut Vec<ConnEv>, logger: &mut Logger) {
-        let mut read_buf: [u8; 512] = [0; 512];
-
-        if let Some(stream) = self.status.get_stream_mut() {
-            match stream.read_ready(&mut read_buf) {
-                Err(err) => {
-                    if !err.is_would_block() {
-                        evs.push(ConnEv::Err(err));
-                    }
+        QUIT { chans, .. } => {
+            let nick = match pfx {
+                Some(User { ref nick, .. }) => nick,
+                _ => {
+                    // TODO: log this?
+                    return;
                 }
-                Ok(bytes_read) => {
-                    self.reset_ticks();
-                    self.in_buf.extend(&read_buf[0..bytes_read]);
-                    self.handle_msgs(evs, logger);
-                }
-            }
-        }
-    }
+            };
 
-    fn handle_msgs(&mut self, evs: &mut Vec<ConnEv>, logger: &mut Logger) {
-        while let Some(msg) = Msg::read(
-            &mut self.in_buf,
-            Some(logger.get_raw_serv_logs(&self.serv_addr)),
-        ) {
-            self.handle_msg(msg, evs, logger);
+            let serv = client.get_serv_name();
+            for chan in &chans {
+                ui.remove_nick(nick, Some(time::now()), &MsgTarget::Chan { serv, chan });
+            }
+            if ui.user_tab_exists(serv, nick) {
+                ui.remove_nick(nick, Some(time::now()), &MsgTarget::User { serv, nick });
+            }
         }
-    }
 
-    fn handle_msg(&mut self, msg: Msg, evs: &mut Vec<ConnEv>, logger: &mut Logger) {
-        if let Msg {
-            cmd:
-                Cmd::CAP {
-                    client: _,
-                    ref subcommand,
-                    ref params,
-                },
-            ..
-        } = msg
-        {
-            match subcommand.as_ref() {
-                "ACK" => {
-                    if params.iter().any(|cap| cap.as_str() == "sasl") {
-                        self.status.get_stream_mut().map(|stream| {
-                            wire::authenticate(stream, "PLAIN").unwrap();
-                        });
-                    }
-                }
-                "NAK" => {
-                    self.end_capability_negotiation();
-                }
-                "LS" => {
-                    if let Some(stream) = self.status.get_stream_mut() {
-                        introduce(stream, None, &self.hostname, &self.realname, &self.nicks[0]);
-                        if params.iter().any(|cap| cap == "sasl") {
-                            wire::cap_req(stream, &["sasl"]).unwrap();
-                            // Will wait for CAP ... ACK from server before authentication.
-                        }
-                    }
+        NICK { nick, chans } => {
+            let old_nick = match pfx {
+                Some(User { nick, .. }) => nick,
+                _ => {
+                    // TODO: log this?
+                    return;
                 }
-                _ => {}
             };
-        }
 
-        if let Msg {
-            cmd: Cmd::AUTHENTICATE { ref param },
-            ..
-        } = msg
-        {
-            if param.as_str() == "+" {
-                // Empty AUTHENTICATE response.  It means server accepted the specified SASL
-                // mechanism (PLAIN)
-                self.plain_sasl_authenticate();
+            let serv = client.get_serv_name();
+            for chan in &chans {
+                ui.rename_nick(
+                    &old_nick,
+                    &nick,
+                    time::now(),
+                    &MsgTarget::Chan { serv, chan },
+                );
+            }
+            if ui.user_tab_exists(serv, &old_nick) {
+                ui.rename_nick(
+                    &old_nick,
+                    &nick,
+                    time::now(),
+                    &MsgTarget::User {
+                        serv,
+                        nick: &old_nick,
+                    },
+                );
             }
         }
 
-        match msg.cmd {
-            // 903: RPL_SASLSUCCESS, 904: ERR_SASLFAIL
-            Cmd::Reply { num: 903, .. } | Cmd::Reply { num: 904, .. } => {
-                self.end_capability_negotiation();
+        Reply { num: 433, .. } => {
+            // ERR_NICKNAMEINUSE
+            if client.is_nick_accepted() {
+                // Nick change request from user failed. Just show an error message.
+                ui.add_err_msg(
+                    "Nickname is already in use",
+                    time::now(),
+                    &MsgTarget::AllServTabs {
+                        serv: client.get_serv_name(),
+                    },
+                );
             }
-            _ => {}
         }
 
-        if let Msg {
-            cmd: Cmd::PING { ref server },
-            ..
-        } = msg
-        {
-            self.status.get_stream_mut().map(|stream| {
-                wire::pong(stream, server).unwrap();
-            });
+        PING { .. } | PONG { .. } => {
+            // Ignore
         }
 
-        if let Msg {
-            cmd: Cmd::JOIN { .. },
-            pfx: Some(Pfx::User { ref nick, ref user }),
-        } = msg
-        {
-            if nick == self.get_nick() {
-                let usermask = format!("{}!{}", nick, user);
-                logger
-                    .get_debug_logs()
-                    .write_line(format_args!("usermask set: {}", usermask));
-                self.usermask = Some(usermask);
-            }
+        ERROR { msg } => {
+            ui.add_err_msg(
+                &msg,
+                time::now(),
+                &MsgTarget::AllServTabs {
+                    serv: client.get_serv_name(),
+                },
+            );
         }
 
-        if let Msg {
-            cmd: Cmd::Reply {
-                num: 396,
-                ref params,
-            },
-            ..
-        } = msg
-        {
-            // :hobana.freenode.net 396 osa1 haskell/developer/osa1
-            // :is now your hidden host (set by services.)
-            if params.len() == 3 {
-                let usermask = format!("{}!~{}@{}", self.get_nick(), self.hostname, params[1]);
-                logger
-                    .get_debug_logs()
-                    .write_line(format_args!("usermask set: {}", usermask));
-                self.usermask = Some(usermask);
-            }
+        TOPIC { chan, topic } => {
+            ui.set_topic(&topic, time::now(), client.get_serv_name(), &chan);
         }
 
-        if let Msg {
-            cmd: Cmd::Reply {
-                num: 302,
-                ref params,
-            },
-            ..
-        } = msg
-        {
-            // 302 RPL_USERHOST
-            // :ircd.stealth.net 302 yournick :syrk=+syrk@millennium.stealth.net
-            //
-            // We know there will be only one nick because /userhost cmd sends
-            // one parameter (our nick)
-            //
-            // Example args: ["osa1", "osa1=+omer@moz-s8a.9ac.93.91.IP "]
-
-            let param = &params[1];
-            match wire::find_byte(param.as_bytes(), b'=') {
-                None => {
-                    logger
-                        .get_debug_logs()
-                        .write_line(format_args!("can't parse RPL_USERHOST: {}", params[1]));
+        CAP {
+            client: _,
+            subcommand,
+            params,
+        } => {
+            match subcommand.as_ref() {
+                "NAK" => {
+                    if params.iter().any(|cap| cap.as_str() == "sasl") {
+                        let msg_target = MsgTarget::Server {
+                            serv: client.get_serv_name(),
+                        };
+                        ui.add_err_msg(
+                            "Server rejected using SASL authenication capability",
+                            time::now(),
+                            &msg_target,
+                        );
+                    }
                 }
-                Some(mut i) => {
-                    if param.as_bytes().get(i + 1) == Some(&b'+')
-                        || param.as_bytes().get(i + 1) == Some(&b'-')
-                    {
-                        i += 1;
+                "LS" => {
+                    if !params.iter().any(|cap| cap.as_str() == "sasl") {
+                        let msg_target = MsgTarget::Server {
+                            serv: client.get_serv_name(),
+                        };
+                        ui.add_err_msg(
+                            "Server does not support SASL authenication",
+                            time::now(),
+                            &msg_target,
+                        );
                     }
-                    let usermask = (&param[i..]).trim();
-                    self.usermask = Some(usermask.to_owned());
-                    logger
-                        .get_debug_logs()
-                        .write_line(format_args!("usermask set: {}", usermask));
+                }
+                "ACK" => {}
+                _cmd => {
+                    // self.logger
+                    //     .get_debug_logs()
+                    //     .write_line(format_args!("CAP subcommand {} is not handled", cmd));
                 }
             }
         }
 
-        if let Msg {
-            cmd: Cmd::Reply { num: 001, .. },
-            ..
-        } = msg
-        {
-            // 001 RPL_WELCOME is how we understand that the registration was successful
-            evs.push(ConnEv::Connected);
-            evs.push(ConnEv::NickChange(self.get_nick().to_owned()));
-            self.nickserv_ident();
-            self.nick_accepted = true;
+        AUTHENTICATE { .. } => {
+            // Ignore
         }
 
-        if let Msg {
-            cmd: Cmd::Reply {
-                num: 002,
-                ref params,
-            },
-            ..
-        } = msg
-        {
-            // 002    RPL_YOURHOST
-            //        "Your host is <servername>, running version <ver>"
-
-            // An example <servername>: cherryh.freenode.net[149.56.134.238/8001]
+        Reply { num: n, params } => {
+            if n <= 003 /* RPL_WELCOME, RPL_YOURHOST, RPL_CREATED */
+                    || n == 251 /* RPL_LUSERCLIENT */
+                    || n == 255 /* RPL_LUSERME */
+                    || n == 372 /* RPL_MOTD */
+                    || n == 375 /* RPL_MOTDSTART */
+                    || n == 376
+            /* RPL_ENDOFMOTD */
+            {
+                debug_assert_eq!(params.len(), 2);
+                let msg = &params[1];
+                ui.add_msg(
+                    msg,
+                    time::now(),
+                    &MsgTarget::Server {
+                        serv: client.get_serv_name(),
+                    },
+                );
+            } else if n == 4 // RPL_MYINFO
+                    || n == 5 // RPL_BOUNCE
+                    || (n >= 252 && n <= 254)
+            /* RPL_LUSEROP, RPL_LUSERUNKNOWN, */
+            /* RPL_LUSERCHANNELS */
+            {
+                let msg = params.into_iter().collect::<Vec<String>>().join(" ");
+                ui.add_msg(
+                    &msg,
+                    time::now(),
+                    &MsgTarget::Server {
+                        serv: client.get_serv_name(),
+                    },
+                );
+            } else if n == 265 || n == 266 || n == 250 {
+                let msg = &params[params.len() - 1];
+                ui.add_msg(
+                    msg,
+                    time::now(),
+                    &MsgTarget::Server {
+                        serv: client.get_serv_name(),
+                    },
+                );
+            }
+            // RPL_TOPIC
+            else if n == 332 {
+                // FIXME: RFC 2812 says this will have 2 arguments, but freenode
+                // sends 3 arguments (extra one being our nick).
+                assert!(params.len() == 3 || params.len() == 2);
+                let chan = &params[params.len() - 2];
+                let topic = &params[params.len() - 1];
+                ui.set_topic(topic, time::now(), client.get_serv_name(), chan);
+            }
+            // RPL_NAMREPLY: List of users in a channel
+            else if n == 353 {
+                let chan = &params[2];
+                let chan_target = MsgTarget::Chan {
+                    serv: client.get_serv_name(),
+                    chan,
+                };
 
-            match parse_servername(params) {
-                None => {
-                    logger.get_debug_logs().write_line(format_args!(
-                        "{} Can't parse hostname from params: {:?}",
-                        self.serv_addr, params
-                    ));
-                }
-                Some(servername) => {
-                    logger
-                        .get_debug_logs()
-                        .write_line(format_args!("{} host: {}", self.serv_addr, servername));
-                    self.servername = Some(servername);
+                for nick in params[3].split_whitespace() {
+                    ui.add_nick(wire::drop_nick_prefix(nick), None, &chan_target);
                 }
             }
-        }
-
-        if let Msg {
-            cmd: Cmd::Reply { num: 433, .. },
-            ..
-        } = msg
-        {
-            // ERR_NICKNAMEINUSE
-            if !self.nick_accepted {
-                self.next_nick();
+            // RPL_ENDOFNAMES: End of NAMES list
+            else if n == 366 {
             }
-        }
-
-        if let Msg {
-            cmd: Cmd::NICK { nick: ref new_nick },
-            pfx: Some(Pfx::User {
-                nick: ref old_nick, ..
-            }),
-        } = msg
-        {
-            if old_nick == self.get_nick() {
-                self.set_nick(new_nick);
-                evs.push(ConnEv::NickChange(self.get_nick().to_owned()));
-                self.nickserv_ident();
+            // RPL_UNAWAY or RPL_NOWAWAY
+            else if n == 305 || n == 306 {
+                let msg = &params[1];
+                ui.add_client_msg(
+                    msg,
+                    &MsgTarget::AllServTabs {
+                        serv: client.get_serv_name(),
+                    },
+                );
             }
-        }
-
-        if let Msg {
-            cmd: Cmd::Reply { num: 376, .. },
-            ..
-        } = msg
-        {
-            if let Some(mut stream) = self.status.get_stream_mut() {
-                // RPL_ENDOFMOTD. Join auto-join channels.
-                if !self.auto_join.is_empty() {
-                    wire::join(
-                        &mut stream,
-                        self.auto_join
-                            .iter()
-                            .map(String::as_str)
-                            .collect::<Vec<&str>>()
-                            .as_slice(),
-                    )
-                    .unwrap();
-                }
-
-                // Set away mode
-                if let Some(ref reason) = self.away_status {
-                    wire::away(stream, Some(reason)).unwrap();
+            // ERR_NOSUCHNICK
+            else if n == 401 {
+                let nick = &params[1];
+                let msg = &params[2];
+                let serv = client.get_serv_name();
+                ui.add_client_msg(msg, &MsgTarget::User { serv, nick });
+            // RPL_AWAY
+            } else if n == 301 {
+                let serv = client.get_serv_name();
+                let nick = &params[1];
+                let msg = &params[2];
+                ui.add_client_msg(
+                    &format!("{} is away: {}", nick, msg),
+                    &MsgTarget::User { serv, nick },
+                );
+            } else {
+                match pfx {
+                    Some(Server(msg_serv)) => {
+                        let conn_serv_name = client.get_serv_name();
+                        let msg_target = MsgTarget::Server {
+                            serv: conn_serv_name,
+                        };
+                        ui.add_privmsg(
+                            &msg_serv,
+                            &params.join(" "),
+                            time::now(),
+                            &msg_target,
+                            false,
+                            false,
+                        );
+                        ui.set_tab_style(TabStyle::NewMsg, &msg_target);
+                    }
+                    _pfx => {
+                        // add everything else to debug file
+                        // self.logger.get_debug_logs().write_line(format_args!(
+                        //     "Ignoring numeric reply msg:\nPfx: {:?}, num: {:?}, args: {:?}",
+                        //     pfx, n, params
+                        // ));
+                    }
                 }
             }
         }
 
-        if let Msg {
-            cmd: Cmd::Reply {
-                num: 332,
-                ref params,
-            },
-            ..
-        } = msg
-        {
-            if params.len() == 2 || params.len() == 3 {
-                // RPL_TOPIC. We've successfully joined a channel, add the channel to
-                // self.auto_join to be able to auto-join next time we connect
-                let chan = &params[params.len() - 2];
-                if !self.auto_join.contains(chan) {
-                    self.auto_join.push(chan.to_owned());
-                }
+        Other { cmd: _, params } => match pfx {
+            Some(Server(msg_serv)) => {
+                let conn_serv_name = client.get_serv_name();
+                let msg_target = MsgTarget::Server {
+                    serv: conn_serv_name,
+                };
+                ui.add_privmsg(
+                    &msg_serv,
+                    &params.join(" "),
+                    time::now(),
+                    &msg_target,
+                    false,
+                    false,
+                );
+                ui.set_tab_style(TabStyle::NewMsg, &msg_target);
             }
-        }
-
-        evs.push(ConnEv::Msg(msg));
-    }
-}
-
-/// Try to parse servername in a 002 RPL_YOURHOST reply
-fn parse_servername(params: &[String]) -> Option<String> {
-    let msg = params.get(1).or_else(|| params.get(0))?;
-    let slice1 = &msg[13..];
-    let servername_ends = wire::find_byte(slice1.as_bytes(), b'[')
-        .or_else(|| wire::find_byte(slice1.as_bytes(), b','))?;
-    Some((&slice1[..servername_ends]).to_owned())
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_parse_servername_1() {
-        let args = vec![
-            "tiny_test".to_owned(),
-            "Your host is adams.freenode.net[94.125.182.252/8001], \
-             running version ircd-seven-1.1.4"
-                .to_owned(),
-        ];
-        assert_eq!(
-            parse_servername(&args),
-            Some("adams.freenode.net".to_owned())
-        );
-    }
-
-    #[test]
-    fn test_parse_servername_2() {
-        let args = vec![
-            "tiny_test".to_owned(),
-            "Your host is belew.mozilla.org, running version InspIRCd-2.0".to_owned(),
-        ];
-        assert_eq!(
-            parse_servername(&args),
-            Some("belew.mozilla.org".to_owned())
-        );
+            _pfx => {
+                // self.logger.get_debug_logs().write_line(format_args!(
+                //     "Ignoring msg:\nPfx: {:?}, msg: {} :{}",
+                //     pfx,
+                //     cmd,
+                //     params.join(" "),
+                // ));
+            }
+        },
     }
 }
diff --git a/src/lib.rs b/src/lib.rs
deleted file mode 100644
index 3e6d43b4..00000000
--- a/src/lib.rs
+++ /dev/null
@@ -1,994 +0,0 @@
-#![cfg_attr(test, feature(test))]
-#![feature(allocator_api)]
-#![feature(const_fn)]
-#![feature(drain_filter)]
-#![feature(nll)]
-#![feature(ptr_offset_from)]
-
-#[global_allocator]
-static ALLOC: std::alloc::System = std::alloc::System;
-
-#[cfg(test)]
-extern crate quickcheck;
-
-extern crate dirs;
-extern crate libc;
-extern crate mio;
-extern crate native_tls;
-extern crate net2;
-extern crate serde;
-extern crate serde_yaml;
-extern crate tempfile;
-extern crate time;
-
-extern crate term_input;
-extern crate termbox_simple;
-
-extern crate take_mut;
-
-#[macro_use]
-mod utils;
-
-mod cmd;
-mod cmd_line_args;
-pub mod config;
-mod conn;
-mod logger;
-mod notifier;
-mod stream;
-pub mod trie;
-pub mod tui;
-mod wire;
-
-use mio::unix::EventedFd;
-use mio::unix::UnixReady;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
-use std::path::PathBuf;
-use std::time::Duration;
-use std::time::Instant;
-
-use cmd::{parse_cmd, ParseCmdResult};
-use cmd_line_args::{parse_cmd_line_args, CmdLineArgs};
-use conn::{Conn, ConnErr, ConnEv};
-use logger::Logger;
-use term_input::{Event, Input};
-use tui::{MsgSource, MsgTarget, TUIRet, TabStyle, Timestamp, TUI};
-use wire::{Cmd, Msg, Pfx};
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-pub fn run() {
-    let CmdLineArgs {
-        servers: server_args,
-        config_path,
-    } = parse_cmd_line_args(std::env::args());
-    let config_path = config_path.unwrap_or_else(config::get_default_config_path);
-    if config_path.is_dir() {
-        println!("The config path is a directory.");
-        ::std::process::exit(1);
-    } else if !config_path.is_file() {
-        config::generate_default_config(&config_path);
-    } else {
-        match config::parse_config(&config_path) {
-            Err(yaml_err) => {
-                println!("Can't parse config file:");
-                println!("{}", yaml_err);
-                ::std::process::exit(1);
-            }
-            Ok(config::Config {
-                servers,
-                defaults,
-                colors,
-                log_dir,
-            }) => {
-                let servers = if !server_args.is_empty() {
-                    // connect only to servers that match at least one of
-                    // the given patterns
-                    servers
-                        .into_iter()
-                        .filter(|s| {
-                            for server in &server_args {
-                                if s.addr.contains(server) {
-                                    return true;
-                                }
-                            }
-                            false
-                        })
-                        .collect()
-                } else {
-                    servers
-                };
-                Tiny::run(servers, defaults, log_dir, colors, config_path)
-            }
-        }
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-pub struct Tiny<'poll> {
-    conns: Vec<Conn<'poll>>,
-    defaults: config::Defaults,
-    tui: TUI,
-    input_ev_handler: Input,
-    logger: Logger,
-    config_path: PathBuf,
-}
-
-const STDIN_TOKEN: Token = Token(libc::STDIN_FILENO as usize);
-
-impl<'poll> Tiny<'poll> {
-    pub fn run(
-        servers: Vec<config::Server>,
-        defaults: config::Defaults,
-        log_dir: String,
-        colors: config::Colors,
-        config_path: PathBuf,
-    ) {
-        let poll = Poll::new().unwrap();
-
-        poll.register(
-            &EventedFd(&libc::STDIN_FILENO),
-            STDIN_TOKEN,
-            Ready::readable(),
-            PollOpt::level(),
-        )
-        .unwrap();
-
-        let mut conns = Vec::with_capacity(servers.len());
-
-        let mut tui = TUI::new(colors);
-
-        // init "mentions" tab
-        tui.new_server_tab("mentions");
-        tui.add_client_msg(
-            "Any mentions to you will be listed here.",
-            &MsgTarget::Server {
-                serv_name: "mentions",
-            },
-        );
-
-        tui.draw();
-
-        for server in servers.iter().cloned() {
-            let msg_target = MsgTarget::Server {
-                serv_name: &server.addr.clone(),
-            };
-            match Conn::new(server, &poll) {
-                Ok(conn) => {
-                    conns.push(conn);
-                }
-                Err(err) => {
-                    tui.add_err_msg(&connect_err_msg(&err), Timestamp::now(), &msg_target);
-                }
-            }
-        }
-
-        let mut tiny = Tiny {
-            conns,
-            defaults,
-            tui,
-            input_ev_handler: Input::new(),
-            logger: Logger::new(PathBuf::from(log_dir)),
-            config_path: config_path.to_owned(),
-        };
-
-        tiny.tui.draw();
-
-        let mut last_tick = Instant::now();
-        let mut poll_evs = Events::with_capacity(10);
-        let mut conn_evs = Vec::with_capacity(10);
-        let mut input_evs = Vec::with_capacity(10);
-        'mainloop: loop {
-            // FIXME this will sometimes miss the tick deadline
-            match poll.poll(&mut poll_evs, Some(Duration::from_secs(1))) {
-                Err(_) => {
-                    // usually SIGWINCH, which is caught by term_input
-                    if tiny.handle_stdin(&poll, &mut input_evs) {
-                        break 'mainloop;
-                    }
-                }
-                Ok(_) => {
-                    for event in poll_evs.iter() {
-                        let token = event.token();
-                        if token == STDIN_TOKEN {
-                            if tiny.handle_stdin(&poll, &mut input_evs) {
-                                break 'mainloop;
-                            }
-                        } else {
-                            match find_token_conn_idx(&tiny.conns, token) {
-                                None => {
-                                    tiny.logger.get_debug_logs().write_line(format_args!(
-                                        "BUG: Can't find Token in conns: {:?}",
-                                        event.token()
-                                    ));
-                                }
-                                Some(conn_idx) => {
-                                    tiny.handle_socket(event.readiness(), conn_idx, &mut conn_evs);
-                                }
-                            }
-                        }
-                    }
-
-                    if last_tick.elapsed() >= Duration::from_secs(1) {
-                        for conn_idx in 0..tiny.conns.len() {
-                            {
-                                let conn = &mut tiny.conns[conn_idx];
-                                conn.tick(&mut conn_evs, tiny.logger.get_debug_logs());
-                            }
-                            tiny.handle_conn_evs(conn_idx, &mut conn_evs);
-                        }
-                        last_tick = Instant::now();
-                    }
-                }
-            }
-
-            tiny.tui.draw();
-        }
-    }
-
-    fn handle_stdin(&mut self, poll: &'poll Poll, evs: &mut Vec<Event>) -> bool {
-        let mut abort = false;
-        self.input_ev_handler.read_input_events(evs);
-        for ev in evs.drain(..) {
-            match self.tui.handle_input_event(ev) {
-                TUIRet::Abort => {
-                    abort = true;
-                }
-                TUIRet::Input { msg, from } => {
-                    self.logger.get_debug_logs().write_line(format_args!(
-                        "Input source: {:#?}, msg: {}",
-                        from,
-                        msg.iter().cloned().collect::<String>()
-                    ));
-
-                    // We know msg has at least one character as the TUI won't accept it otherwise.
-                    if msg[0] == '/' {
-                        let msg_str: String = (&msg[1..]).into_iter().cloned().collect();
-                        self.handle_cmd(poll, from, &msg_str);
-                    } else {
-                        self.send_msg(&from, &msg.into_iter().collect::<String>(), false);
-                    }
-                }
-                TUIRet::Lines { lines, from } => {
-                    self.logger.get_debug_logs().write_line(format_args!(
-                        "Input source: {:#?}, lines: {:?}",
-                        from, lines
-                    ));
-                    for line in lines {
-                        self.send_msg(&from, &line, false);
-                    }
-                }
-                TUIRet::KeyHandled => {}
-                TUIRet::EventIgnored(Event::FocusGained)
-                | TUIRet::EventIgnored(Event::FocusLost) => {}
-                ev => {
-                    self.logger
-                        .get_debug_logs()
-                        .write_line(format_args!("Ignoring event: {:?}", ev));
-                }
-            }
-        }
-        abort
-    }
-
-    fn handle_cmd(&mut self, poll: &'poll Poll, src: MsgSource, msg: &str) {
-        match parse_cmd(msg) {
-            ParseCmdResult::Ok { cmd, rest } => {
-                (cmd.cmd_fn)(rest, poll, self, src);
-            }
-            // ParseCmdResult::Ambiguous(vec) => {
-            //     self.tui.add_client_err_msg(
-            //         &format!("Unsupported command: \"/{}\"", msg),
-            //         &MsgTarget::CurrentTab,
-            //     );
-            //     self.tui.add_client_err_msg(
-            //         &format!("Did you mean one of {:?} ?", vec),
-            //         &MsgTarget::CurrentTab,
-            //     );
-            // },
-            ParseCmdResult::Unknown => self.tui.add_client_err_msg(
-                &format!("Unsupported command: \"/{}\"", msg),
-                &MsgTarget::CurrentTab,
-            ),
-        }
-    }
-
-    fn part(&mut self, serv_name: &str, chan: &str) {
-        let conn = find_conn(&mut self.conns, serv_name).unwrap();
-        conn.part(chan);
-    }
-
-    fn send_msg(&mut self, from: &MsgSource, msg: &str, ctcp_action: bool) {
-        if from.serv_name() == "mentions" {
-            self.tui.add_client_err_msg(
-                "Use `/connect <server>` to connect to a server",
-                &MsgTarget::CurrentTab,
-            );
-            return;
-        }
-
-        // `tui_target`: Where to show the message on TUI
-        // `msg_target`: Actual PRIVMSG target to send to the server
-        // `serv_name`: Server name to find connection in `self.conns`
-        let (tui_target, msg_target, serv_name) = {
-            match from {
-                MsgSource::Serv { ref serv_name } => {
-                    // we don't split raw messages to 512-bytes long chunks
-                    if let Some(conn) = self
-                        .conns
-                        .iter_mut()
-                        .find(|conn| conn.get_serv_name() == serv_name)
-                    {
-                        conn.raw_msg(msg);
-                    } else {
-                        self.tui.add_client_err_msg(
-                            &format!("Can't find server: {}", serv_name),
-                            &MsgTarget::CurrentTab,
-                        );
-                    }
-                    return;
-                }
-
-                MsgSource::Chan {
-                    ref serv_name,
-                    ref chan_name,
-                } => (
-                    MsgTarget::Chan {
-                        serv_name,
-                        chan_name,
-                    },
-                    chan_name,
-                    serv_name,
-                ),
-
-                MsgSource::User {
-                    ref serv_name,
-                    ref nick,
-                } => {
-                    let msg_target = if nick.eq_ignore_ascii_case("nickserv")
-                        || nick.eq_ignore_ascii_case("chanserv")
-                    {
-                        MsgTarget::Server { serv_name }
-                    } else {
-                        MsgTarget::User { serv_name, nick }
-                    };
-                    (msg_target, nick, serv_name)
-                }
-            }
-        };
-
-        let conn = find_conn(&mut self.conns, serv_name).unwrap();
-        let ts = Timestamp::now();
-        let extra_len = msg_target.len() as i32
-            + if ctcp_action {
-                9 // "\0x1ACTION \0x1".len()
-            } else {
-                0
-            };
-        let send_fn = if ctcp_action {
-            Conn::ctcp_action
-        } else {
-            Conn::privmsg
-        };
-        for msg in conn.split_privmsg(extra_len, msg) {
-            send_fn(conn, msg_target, msg);
-            self.tui
-                .add_privmsg(conn.get_nick(), msg, ts, &tui_target, ctcp_action);
-        }
-    }
-
-    ////////////////////////////////////////////////////////////////////////////
-
-    fn handle_socket(&mut self, readiness: Ready, conn_idx: usize, evs: &mut Vec<ConnEv>) {
-        if readiness.is_readable() {
-            self.conns[conn_idx].read_ready(evs, &mut self.logger);
-        }
-        // Handle `ConnEv`s first before checking other readiness events. Reason: we sometimes
-        // realize that the connection is closed/got broken at this point even though a write
-        // readiness event is also available. When this happens we need to enter disconnect state
-        // first, otherwise we end up calling `write_ready()` on a broken/disconnected `Stream`,
-        // which causes a panic. This caused #119.
-        self.handle_conn_evs(conn_idx, evs);
-        // This does nothing if we entered disconnect state in the line above.
-        if readiness.is_writable() {
-            self.conns[conn_idx].write_ready(evs);
-        }
-        if readiness.contains(UnixReady::hup()) {
-            self.conns[conn_idx].enter_disconnect_state();
-            self.tui.add_err_msg(
-                &format!(
-                    "Connection error (HUP). \
-                     Will try to reconnect in {} seconds.",
-                    conn::RECONNECT_TICKS
-                ),
-                Timestamp::now(),
-                &MsgTarget::AllServTabs {
-                    serv_name: self.conns[conn_idx].get_serv_name(),
-                },
-            );
-        }
-        self.handle_conn_evs(conn_idx, evs);
-    }
-
-    fn handle_conn_evs(&mut self, conn_idx: usize, evs: &mut Vec<ConnEv>) {
-        for ev in evs.drain(..) {
-            self.handle_conn_ev(conn_idx, ev);
-        }
-    }
-
-    fn handle_conn_ev(&mut self, conn_idx: usize, ev: ConnEv) {
-        match ev {
-            ConnEv::Connected => {
-                self.tui.add_msg(
-                    "Connected.",
-                    Timestamp::now(),
-                    &MsgTarget::AllServTabs {
-                        serv_name: self.conns[conn_idx].get_serv_name(),
-                    },
-                );
-            }
-            ConnEv::Disconnected => {
-                let conn = &mut self.conns[conn_idx];
-                let target = MsgTarget::AllServTabs {
-                    serv_name: conn.get_serv_name(),
-                };
-                self.tui.add_err_msg(
-                    &format!(
-                        "Disconnected. Will try to reconnect in {} seconds.",
-                        conn::RECONNECT_TICKS
-                    ),
-                    Timestamp::now(),
-                    &target,
-                );
-                self.tui.clear_nicks(&target);
-            }
-            ConnEv::WantReconnect => {
-                let conn = &mut self.conns[conn_idx];
-                self.tui.add_client_msg(
-                    "Connecting...",
-                    &MsgTarget::AllServTabs {
-                        serv_name: conn.get_serv_name(),
-                    },
-                );
-                match conn.reconnect(None) {
-                    Ok(()) => {}
-                    Err(err) => {
-                        self.tui.add_err_msg(
-                            &reconnect_err_msg(&err),
-                            Timestamp::now(),
-                            &MsgTarget::AllServTabs {
-                                serv_name: conn.get_serv_name(),
-                            },
-                        );
-                    }
-                }
-            }
-            ConnEv::Err(err) => {
-                let conn = &mut self.conns[conn_idx];
-                conn.enter_disconnect_state();
-                self.tui.add_err_msg(
-                    &reconnect_err_msg(&err),
-                    Timestamp::now(),
-                    &MsgTarget::AllServTabs {
-                        serv_name: conn.get_serv_name(),
-                    },
-                );
-            }
-            ConnEv::Msg(msg) => {
-                self.handle_msg(conn_idx, msg, Timestamp::now());
-            }
-            ConnEv::NickChange(new_nick) => {
-                let conn = &self.conns[conn_idx];
-                self.tui.set_nick(conn.get_serv_name(), &new_nick);
-            }
-        }
-    }
-
-    fn handle_msg(&mut self, conn_idx: usize, msg: Msg, ts: Timestamp) {
-        let conn = &self.conns[conn_idx];
-        let pfx = msg.pfx;
-        match msg.cmd {
-            Cmd::PRIVMSG {
-                target,
-                msg,
-                is_notice,
-            } => {
-                let pfx = match pfx {
-                    Some(pfx) => pfx,
-                    None => {
-                        self.logger.get_debug_logs().write_line(format_args!(
-                            "PRIVMSG or NOTICE without prefix \
-                             target: {:?} msg: {:?}",
-                            target, msg
-                        ));
-                        return;
-                    }
-                };
-
-                // sender to be shown in the UI
-                let origin = match pfx {
-                    Pfx::Server(_) => conn.get_serv_name(),
-                    Pfx::User { ref nick, .. } => nick,
-                };
-
-                let (msg, is_ctcp_action) = wire::check_ctcp_action_msg(&msg);
-
-                match target {
-                    wire::MsgTarget::Chan(chan) => {
-                        self.logger
-                            .get_chan_logs(conn.get_serv_name(), &chan)
-                            .write_line(format_args!("PRIVMSG: {}", msg));
-                        let msg_target = MsgTarget::Chan {
-                            serv_name: conn.get_serv_name(),
-                            chan_name: &chan,
-                        };
-                        // highlight the message if it mentions us
-                        if msg.find(conn.get_nick()).is_some() {
-                            self.tui.add_privmsg_highlight(
-                                origin,
-                                msg,
-                                ts,
-                                &msg_target,
-                                is_ctcp_action,
-                            );
-                            self.tui.set_tab_style(TabStyle::Highlight, &msg_target);
-                            let mentions_target = MsgTarget::Server {
-                                serv_name: "mentions",
-                            };
-                            self.tui.add_msg(
-                                &format!(
-                                    "{} in {}:{}: {}",
-                                    origin,
-                                    conn.get_serv_name(),
-                                    chan,
-                                    msg
-                                ),
-                                ts,
-                                &mentions_target,
-                            );
-                            self.tui
-                                .set_tab_style(TabStyle::Highlight, &mentions_target);
-                        } else {
-                            self.tui
-                                .add_privmsg(origin, msg, ts, &msg_target, is_ctcp_action);
-                            self.tui.set_tab_style(TabStyle::NewMsg, &msg_target);
-                        }
-                    }
-                    wire::MsgTarget::User(target) => {
-                        let serv_name = conn.get_serv_name();
-                        let msg_target = {
-                            match pfx {
-                                Pfx::Server(_) => MsgTarget::Server { serv_name },
-                                Pfx::User { ref nick, .. } => {
-                                    // show NOTICE messages in server tabs if we don't have a tab
-                                    // for the sender already (see #21)
-                                    if is_notice && !self.tui.does_user_tab_exist(serv_name, nick) {
-                                        MsgTarget::Server { serv_name }
-                                    } else {
-                                        MsgTarget::User { serv_name, nick }
-                                    }
-                                }
-                            }
-                        };
-                        self.tui
-                            .add_privmsg(origin, msg, ts, &msg_target, is_ctcp_action);
-                        if target == conn.get_nick() {
-                            self.tui.set_tab_style(TabStyle::Highlight, &msg_target);
-                        } else {
-                            // not sure if this case can happen
-                            self.tui.set_tab_style(TabStyle::NewMsg, &msg_target);
-                        }
-                    }
-                }
-            }
-
-            Cmd::JOIN { chan } => match pfx {
-                Some(Pfx::User { nick, .. }) => {
-                    let serv_name = conn.get_serv_name();
-                    self.logger
-                        .get_chan_logs(serv_name, &chan)
-                        .write_line(format_args!("JOIN: {}", nick));
-                    if nick == conn.get_nick() {
-                        self.tui.new_chan_tab(serv_name, &chan);
-                    } else {
-                        let nick = drop_nick_prefix(&nick);
-                        let ts = Some(Timestamp::now());
-                        self.tui.add_nick(
-                            nick,
-                            ts,
-                            &MsgTarget::Chan {
-                                serv_name,
-                                chan_name: &chan,
-                            },
-                        );
-                        // Also update the private message tab if it exists
-                        // Nothing will be shown if the user already known to be online by the
-                        // tab
-                        if self.tui.does_user_tab_exist(serv_name, nick) {
-                            self.tui
-                                .add_nick(nick, ts, &MsgTarget::User { serv_name, nick });
-                        }
-                    }
-                }
-                pfx => {
-                    self.logger
-                        .get_debug_logs()
-                        .write_line(format_args!("Weird JOIN message pfx {:?}", pfx));
-                }
-            },
-
-            Cmd::PART { chan, .. } => match pfx {
-                Some(Pfx::User { nick, .. }) => {
-                    if nick != conn.get_nick() {
-                        let serv_name = conn.get_serv_name();
-                        self.logger
-                            .get_chan_logs(serv_name, &chan)
-                            .write_line(format_args!("PART: {}", nick));
-                        self.tui.remove_nick(
-                            &nick,
-                            Some(Timestamp::now()),
-                            &MsgTarget::Chan {
-                                serv_name,
-                                chan_name: &chan,
-                            },
-                        );
-                    }
-                }
-                pfx => {
-                    self.logger
-                        .get_debug_logs()
-                        .write_line(format_args!("Weird PART message pfx {:?}", pfx));
-                }
-            },
-
-            Cmd::QUIT { .. } => match pfx {
-                Some(Pfx::User { ref nick, .. }) => {
-                    let serv_name = conn.get_serv_name();
-                    self.tui.remove_nick(
-                        nick,
-                        Some(Timestamp::now()),
-                        &MsgTarget::AllUserTabs { serv_name, nick },
-                    );
-                }
-                pfx => {
-                    self.logger
-                        .get_debug_logs()
-                        .write_line(format_args!("Weird QUIT message pfx {:?}", pfx));
-                }
-            },
-
-            Cmd::NICK { nick } => match pfx {
-                Some(Pfx::User {
-                    nick: ref old_nick, ..
-                }) => {
-                    let serv_name = conn.get_serv_name();
-                    self.tui.rename_nick(
-                        old_nick,
-                        &nick,
-                        Timestamp::now(),
-                        &MsgTarget::AllUserTabs {
-                            serv_name,
-                            nick: old_nick,
-                        },
-                    );
-                }
-                pfx => {
-                    self.logger
-                        .get_debug_logs()
-                        .write_line(format_args!("Weird NICK message pfx {:?}", pfx));
-                }
-            },
-
-            Cmd::Reply { num: 433, .. } => {
-                // ERR_NICKNAMEINUSE
-                if conn.is_nick_accepted() {
-                    // Nick change request from user failed. Just show an error message.
-                    self.tui.add_err_msg(
-                        "Nickname is already in use",
-                        Timestamp::now(),
-                        &MsgTarget::AllServTabs {
-                            serv_name: conn.get_serv_name(),
-                        },
-                    );
-                }
-            }
-
-            Cmd::PING { .. } | Cmd::PONG { .. } =>
-                // ignore
-                {}
-
-            Cmd::ERROR { ref msg } => {
-                let serv_name = conn.get_serv_name();
-                self.tui
-                    .add_err_msg(msg, Timestamp::now(), &MsgTarget::AllServTabs { serv_name });
-            }
-
-            Cmd::TOPIC {
-                ref chan,
-                ref topic,
-            } => {
-                self.tui.show_topic(
-                    topic,
-                    Timestamp::now(),
-                    &MsgTarget::Chan {
-                        serv_name: conn.get_serv_name(),
-                        chan_name: chan,
-                    },
-                );
-            }
-
-            Cmd::CAP {
-                client: _,
-                ref subcommand,
-                ref params,
-            } => {
-                match subcommand.as_ref() {
-                    "NAK" => {
-                        if params.iter().any(|cap| cap.as_str() == "sasl") {
-                            let msg_target = MsgTarget::Server {
-                                serv_name: conn.get_serv_name(),
-                            };
-                            self.tui.add_err_msg(
-                                "Server rejected using SASL authenication capability",
-                                Timestamp::now(),
-                                &msg_target,
-                            );
-                        }
-                    }
-                    "LS" => {
-                        if !params.iter().any(|cap| cap.as_str() == "sasl") {
-                            let msg_target = MsgTarget::Server {
-                                serv_name: conn.get_serv_name(),
-                            };
-                            self.tui.add_err_msg(
-                                "Server does not support SASL authenication",
-                                Timestamp::now(),
-                                &msg_target,
-                            );
-                        }
-                    }
-                    "ACK" => {}
-                    cmd => {
-                        self.logger
-                            .get_debug_logs()
-                            .write_line(format_args!("CAP subcommand {} is not handled", cmd));
-                    }
-                };
-            }
-
-            Cmd::AUTHENTICATE { .. } =>
-                // ignore
-                {}
-
-            Cmd::Reply { num: n, params } => {
-                if n <= 003 /* RPL_WELCOME, RPL_YOURHOST, RPL_CREATED */
-                        || n == 251 /* RPL_LUSERCLIENT */
-                        || n == 255 /* RPL_LUSERME */
-                        || n == 372 /* RPL_MOTD */
-                        || n == 375 /* RPL_MOTDSTART */
-                        || n == 376
-                /* RPL_ENDOFMOTD */
-                {
-                    debug_assert_eq!(params.len(), 2);
-                    let msg = &params[1];
-                    self.tui.add_msg(
-                        msg,
-                        Timestamp::now(),
-                        &MsgTarget::Server {
-                            serv_name: conn.get_serv_name(),
-                        },
-                    );
-                } else if n == 4 // RPL_MYINFO
-                        || n == 5 // RPL_BOUNCE
-                        || (n >= 252 && n <= 254)
-                /* RPL_LUSEROP, RPL_LUSERUNKNOWN, */
-                /* RPL_LUSERCHANNELS */
-                {
-                    let msg = params.into_iter().collect::<Vec<String>>().join(" ");
-                    self.tui.add_msg(
-                        &msg,
-                        Timestamp::now(),
-                        &MsgTarget::Server {
-                            serv_name: conn.get_serv_name(),
-                        },
-                    );
-                } else if n == 265 || n == 266 || n == 250 {
-                    let msg = &params[params.len() - 1];
-                    self.tui.add_msg(
-                        msg,
-                        Timestamp::now(),
-                        &MsgTarget::Server {
-                            serv_name: conn.get_serv_name(),
-                        },
-                    );
-                }
-                // RPL_TOPIC
-                else if n == 332 {
-                    // FIXME: RFC 2812 says this will have 2 arguments, but freenode
-                    // sends 3 arguments (extra one being our nick).
-                    assert!(params.len() == 3 || params.len() == 2);
-                    let chan = &params[params.len() - 2];
-                    let topic = &params[params.len() - 1];
-                    self.tui.show_topic(
-                        topic,
-                        Timestamp::now(),
-                        &MsgTarget::Chan {
-                            serv_name: conn.get_serv_name(),
-                            chan_name: chan,
-                        },
-                    );
-                }
-                // RPL_NAMREPLY: List of users in a channel
-                else if n == 353 {
-                    let chan = &params[2];
-                    let chan_target = MsgTarget::Chan {
-                        serv_name: conn.get_serv_name(),
-                        chan_name: chan,
-                    };
-
-                    for nick in params[3].split_whitespace() {
-                        self.tui
-                            .add_nick(drop_nick_prefix(nick), None, &chan_target);
-                    }
-                }
-                // RPL_ENDOFNAMES: End of NAMES list
-                else if n == 366 {
-                }
-                // RPL_UNAWAY or RPL_NOWAWAY
-                else if n == 305 || n == 306 {
-                    let msg = &params[1];
-                    self.tui.add_client_msg(
-                        msg,
-                        &MsgTarget::AllServTabs {
-                            serv_name: conn.get_serv_name(),
-                        },
-                    );
-                }
-                // ERR_NOSUCHNICK
-                else if n == 401 {
-                    let nick = &params[1];
-                    let msg = &params[2];
-                    let serv_name = conn.get_serv_name();
-                    self.tui
-                        .add_client_msg(msg, &MsgTarget::User { serv_name, nick });
-                // RPL_AWAY
-                } else if n == 301 {
-                    let serv_name = conn.get_serv_name();
-                    let nick = &params[1];
-                    let msg = &params[2];
-                    self.tui.add_client_msg(
-                        &format!("{} is away: {}", nick, msg),
-                        &MsgTarget::User { serv_name, nick },
-                    );
-                } else {
-                    match pfx {
-                        Some(Pfx::Server(msg_serv_name)) => {
-                            let conn_serv_name = conn.get_serv_name();
-                            let msg_target = MsgTarget::Server {
-                                serv_name: conn_serv_name,
-                            };
-                            self.tui.add_privmsg(
-                                &msg_serv_name,
-                                &params.join(" "),
-                                Timestamp::now(),
-                                &msg_target,
-                                false,
-                            );
-                            self.tui.set_tab_style(TabStyle::NewMsg, &msg_target);
-                        }
-                        pfx => {
-                            // add everything else to debug file
-                            self.logger.get_debug_logs().write_line(format_args!(
-                                "Ignoring numeric reply msg:\nPfx: {:?}, num: {:?}, args: {:?}",
-                                pfx, n, params
-                            ));
-                        }
-                    }
-                }
-            }
-
-            Cmd::Other { cmd, params } => match pfx {
-                Some(Pfx::Server(msg_serv_name)) => {
-                    let conn_serv_name = conn.get_serv_name();
-                    let msg_target = MsgTarget::Server {
-                        serv_name: conn_serv_name,
-                    };
-                    self.tui.add_privmsg(
-                        &msg_serv_name,
-                        &params.join(" "),
-                        Timestamp::now(),
-                        &msg_target,
-                        false,
-                    );
-                    self.tui.set_tab_style(TabStyle::NewMsg, &msg_target);
-                }
-                pfx => {
-                    self.logger.get_debug_logs().write_line(format_args!(
-                        "Ignoring msg:\nPfx: {:?}, msg: {} :{}",
-                        pfx,
-                        cmd,
-                        params.join(" "),
-                    ));
-                }
-            },
-        }
-    }
-}
-
-fn find_token_conn_idx(conns: &[Conn], token: Token) -> Option<usize> {
-    for (conn_idx, conn) in conns.iter().enumerate() {
-        if conn.get_conn_tok() == Some(token) {
-            return Some(conn_idx);
-        }
-    }
-    None
-}
-
-fn find_conn<'a, 'poll>(
-    conns: &'a mut [Conn<'poll>],
-    serv_name: &str,
-) -> Option<&'a mut Conn<'poll>> {
-    match find_conn_idx(conns, serv_name) {
-        None => None,
-        Some(idx) => Some(&mut conns[idx]),
-    }
-}
-
-fn find_conn_idx(conns: &[Conn], serv_name: &str) -> Option<usize> {
-    for (conn_idx, conn) in conns.iter().enumerate() {
-        if conn.get_serv_name() == serv_name {
-            return Some(conn_idx);
-        }
-    }
-    None
-}
-
-fn connect_err_msg(err: &ConnErr) -> String {
-    match err.source() {
-        Some(other_err) => format!(
-            "Connection error: {} ({})",
-            err.description(),
-            other_err.description()
-        ),
-        None => format!("Connection error: {}", err.description()),
-    }
-}
-
-fn reconnect_err_msg(err: &ConnErr) -> String {
-    match err.source() {
-        Some(other_err) => format!(
-            "Connection error: {} ({}). \
-             Will try to reconnect in {} seconds.",
-            err.description(),
-            other_err.description(),
-            conn::RECONNECT_TICKS
-        ),
-        None => format!(
-            "Connection error: {}. \
-             Will try to reconnect in {} seconds.",
-            err.description(),
-            conn::RECONNECT_TICKS
-        ),
-    }
-}
-
-/// Nicks may have prefixes, indicating it is a operator, founder, or
-/// something else.
-/// Channel Membership Prefixes:
-/// http://modern.ircdocs.horse/#channel-membership-prefixes
-///
-/// Returns the nick without prefix
-fn drop_nick_prefix(nick: &str) -> &str {
-    static PREFIXES: [char; 5] = ['~', '&', '@', '%', '+'];
-
-    if PREFIXES.contains(&nick.chars().nth(0).unwrap()) {
-        &nick[1..]
-    } else {
-        nick
-    }
-}
diff --git a/src/logger.rs b/src/logger.rs
deleted file mode 100644
index 1738c552..00000000
--- a/src/logger.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-use std::fmt;
-use std::fs;
-use std::fs::File;
-use std::fs::OpenOptions;
-use std::io::Write;
-use std::path::PathBuf;
-use time;
-
-// Using Vec in `fds` for custom lookup functions that don't allocate. This is probably also
-// possible with HashMap if the internals are exposed. Alternatively I guess we could implement a
-// `Borrow` instance for `LogDest`.
-
-pub struct Logger {
-    log_dir: PathBuf,
-    fds: Vec<(LogDest, File)>,
-    debug_fd: File,
-}
-
-pub struct LogFile<'a> {
-    fd: &'a File,
-}
-
-impl<'a> LogFile<'a> {
-    pub fn write_line(&mut self, args: fmt::Arguments) {
-        let now = time::now();
-        write!(self.fd, "[{}] ", now.rfc822()).unwrap();
-        self.fd.write_fmt(args).unwrap();
-        writeln!(self.fd).unwrap();
-    }
-}
-
-/// Log message destination
-enum LogDest {
-    // Server(String),
-    Chan {
-        serv: String,
-        chan: String,
-    },
-    /// For logging raw messages
-    ServerRaw(String),
-}
-
-fn init_log_file(file: &mut dyn Write) {
-    let now = time::now();
-    writeln!(file, "\nLogs started on {}\n", now.rfc822()).unwrap();
-}
-
-impl Logger {
-    pub fn new(log_dir: PathBuf) -> Logger {
-        let _ = fs::create_dir_all(&log_dir);
-
-        let debug_logs = {
-            let mut log_dir = log_dir.clone();
-            log_dir.push("debug.log");
-            let mut file = OpenOptions::new()
-                .append(true)
-                .create(true)
-                .open(log_dir)
-                .unwrap();
-            init_log_file(&mut file);
-            file
-        };
-
-        Logger {
-            log_dir,
-            fds: vec![],
-            debug_fd: debug_logs,
-        }
-    }
-
-    // Stupid code below because of
-    // https://users.rust-lang.org/t/weird-borrow-checker-error-for-loop-keeps-references-after-its-scope-ends/10929
-
-    /*
-        pub fn get_serv_logs(&mut self, serv_: &str) -> LogFile {
-            let pos = self.fds.iter().position(|&(ref dest, _)| {
-                if let &LogDest::Server(ref serv) = dest {
-                    serv == serv_
-                } else {
-                    false
-                }
-            });
-
-            match pos {
-                Some(idx) => LogFile { fd: &mut self.fds[idx].1 },
-                None => {
-                    let mut log_path = self.log_dir.clone();
-                    log_path.push(format!("{}.log", serv_));
-                    let mut file = OpenOptions::new().append(true).create(true).open(log_path).unwrap();
-                    init_log_file(&mut file);
-                    let idx = self.fds.len();
-                    self.fds.push((LogDest::Server(serv_.to_owned()), file));
-                    LogFile { fd: &mut self.fds[idx].1 }
-                }
-            }
-        }
-    */
-
-    pub fn get_chan_logs(&mut self, serv_: &str, chan_: &str) -> LogFile {
-        let pos = self.fds.iter().position(|&(ref dest, _)| {
-            if let LogDest::Chan { ref serv, ref chan } = *dest {
-                serv == serv_ && chan == chan_
-            } else {
-                false
-            }
-        });
-
-        match pos {
-            Some(idx) => LogFile {
-                fd: &mut self.fds[idx].1,
-            },
-            None => {
-                let mut log_path = self.log_dir.clone();
-                log_path.push(format!("{}_{}.log", serv_, chan_));
-                let mut file = OpenOptions::new()
-                    .append(true)
-                    .create(true)
-                    .open(log_path)
-                    .unwrap();
-                init_log_file(&mut file);
-                let idx = self.fds.len();
-                self.fds.push((
-                    LogDest::Chan {
-                        serv: serv_.to_owned(),
-                        chan: chan_.to_owned(),
-                    },
-                    file,
-                ));
-                LogFile {
-                    fd: &mut self.fds[idx].1,
-                }
-            }
-        }
-    }
-
-    pub fn get_raw_serv_logs(&mut self, serv_: &str) -> LogFile {
-        let pos = self.fds.iter().position(|&(ref dest, _)| {
-            if let LogDest::ServerRaw(ref serv) = *dest {
-                serv == serv_
-            } else {
-                false
-            }
-        });
-
-        match pos {
-            Some(idx) => LogFile {
-                fd: &mut self.fds[idx].1,
-            },
-            None => {
-                let mut log_path = self.log_dir.clone();
-                log_path.push(format!("{}_raw.log", serv_));
-                let mut file = OpenOptions::new()
-                    .append(true)
-                    .create(true)
-                    .open(log_path)
-                    .unwrap();
-                init_log_file(&mut file);
-                let idx = self.fds.len();
-                self.fds.push((LogDest::ServerRaw(serv_.to_owned()), file));
-                LogFile {
-                    fd: &mut self.fds[idx].1,
-                }
-            }
-        }
-    }
-
-    pub fn get_debug_logs(&mut self) -> LogFile {
-        LogFile {
-            fd: &mut self.debug_fd,
-        }
-    }
-}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 00000000..3291f753
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,158 @@
+#![cfg_attr(test, feature(test))]
+#![feature(drain_filter)]
+#![feature(ptr_offset_from)]
+#![allow(clippy::zero_prefixed_literal)]
+
+mod cmd;
+mod cmd_line_args;
+mod config;
+mod conn;
+mod ui;
+mod utils;
+
+use cmd_line_args::{parse_cmd_line_args, CmdLineArgs};
+use libtiny_client::{Client, ServerInfo};
+use libtiny_logger::Logger;
+use libtiny_tui::{Colors, MsgTarget, TUI};
+use libtiny_ui::UI;
+use std::path::PathBuf;
+
+fn main() {
+    let CmdLineArgs {
+        servers: server_args,
+        config_path,
+    } = parse_cmd_line_args(std::env::args());
+    let config_path = config_path.unwrap_or_else(config::get_default_config_path);
+    if config_path.is_dir() {
+        println!("The config path is a directory.");
+        ::std::process::exit(1);
+    } else if !config_path.is_file() {
+        config::generate_default_config(&config_path);
+    } else {
+        match config::parse_config(&config_path) {
+            Err(yaml_err) => {
+                println!("Can't parse config file:");
+                println!("{}", yaml_err);
+                ::std::process::exit(1);
+            }
+            Ok(config::Config {
+                servers,
+                defaults,
+                colors,
+                log_dir,
+            }) => {
+                let servers = if !server_args.is_empty() {
+                    // connect only to servers that match at least one of
+                    // the given patterns
+                    servers
+                        .into_iter()
+                        .filter(|s| {
+                            for server in &server_args {
+                                if s.addr.contains(server) {
+                                    return true;
+                                }
+                            }
+                            false
+                        })
+                        .collect()
+                } else {
+                    servers
+                };
+                run(servers, defaults, colors, config_path, log_dir)
+            }
+        }
+    }
+}
+
+fn run(
+    servers: Vec<config::Server>,
+    defaults: config::Defaults,
+    colors: Colors,
+    config_path: PathBuf,
+    log_dir: Option<PathBuf>,
+) {
+    env_logger::builder()
+        .target(env_logger::Target::Stderr)
+        .init();
+
+    // One task for each client to handle IRC events
+    // One task for TUI events
+    let mut executor = tokio::runtime::current_thread::Runtime::new().unwrap();
+
+    // Create TUI task
+    let (tui, rcv_tui_ev) = TUI::run(colors, &mut executor);
+
+    // init "mentions" tab
+    tui.new_server_tab("mentions");
+    tui.add_client_msg(
+        "Any mentions to you will be listed here.",
+        &MsgTarget::Server { serv: "mentions" },
+    );
+    tui.draw();
+
+    // Create logger
+    let report_logger_error = {
+        let tui_clone = tui.clone();
+        Box::new(move |err: String| {
+            // Somehwat hacky -- only tab we have is "mentions" so we show the error there
+            tui_clone.add_client_err_msg(
+                &format!("Logger error: {}", err),
+                &MsgTarget::Server { serv: "mentions" },
+            )
+        })
+    };
+    let logger: Option<Logger> =
+        log_dir.and_then(|log_dir| match Logger::new(log_dir, report_logger_error) {
+            Err(err) => {
+                tui.add_client_err_msg(
+                    &format!("Can't create logger: {}", err),
+                    &MsgTarget::CurrentTab,
+                );
+                None
+            }
+            Ok(logger) => Some(logger),
+        });
+
+    let tui: Box<dyn UI> = match logger {
+        None => Box::new(tui) as Box<dyn UI>,
+        Some(logger) => Box::new(libtiny_ui::combine(tui, logger)) as Box<dyn UI>,
+    };
+
+    let mut clients: Vec<Client> = Vec::with_capacity(servers.len());
+
+    for server in servers.iter().cloned() {
+        tui.new_server_tab(&server.addr);
+
+        let server_info = ServerInfo {
+            addr: server.addr,
+            port: server.port,
+            tls: server.tls,
+            pass: server.pass,
+            hostname: server.hostname,
+            realname: server.realname,
+            nicks: server.nicks,
+            auto_join: server.join,
+            nickserv_ident: server.nickserv_ident,
+            sasl_auth: server.sasl_auth.map(|auth| libtiny_client::SASLAuth {
+                username: auth.username,
+                password: auth.password,
+            }),
+        };
+
+        let (client, rcv_conn_ev) = Client::new(server_info, Some(&mut executor));
+        // TODO: Somehow it's quite hard to expose this objekt call with a different name and less
+        // polymorphic type in libtiny_ui ...
+        let tui_clone = libtiny_ui::clone_box(&*tui);
+        let client_clone = client.clone();
+
+        // Spawn a task to handle connection events
+        executor.spawn(conn::task(rcv_conn_ev, tui_clone, client_clone));
+
+        clients.push(client);
+    }
+
+    // Spawn a task to handle TUI events
+    executor.spawn(ui::task(config_path, defaults, tui, clients, rcv_tui_ev));
+
+    executor.run().unwrap(); // unwraps RunError
+}
diff --git a/src/stream/mod.rs b/src/stream/mod.rs
deleted file mode 100644
index 75f92645..00000000
--- a/src/stream/mod.rs
+++ /dev/null
@@ -1,147 +0,0 @@
-//! Provides an abstraction over the standard `TcpStream` and `native_tls`'s `TlsStream`.
-
-pub mod tcp;
-pub mod tls;
-pub mod utils;
-
-use self::tcp::TcpError;
-pub use self::tcp::TcpStream;
-use self::tls::TlsError;
-pub use self::tls::TlsStream;
-use mio::Poll;
-use mio::Token;
-use native_tls;
-use std::error;
-use std::error::Error;
-use std::io;
-pub use std::io::Error as IoError;
-use std::io::Write;
-use std::result;
-
-pub enum Stream<'poll> {
-    Tcp(TcpStream<'poll>),
-    Tls(TlsStream<'poll>),
-}
-
-#[derive(Debug)]
-pub enum StreamErr {
-    IoError(IoError),
-    CantResolveAddr,
-    TlsError(native_tls::Error),
-    ConnectionClosed,
-}
-
-impl StreamErr {
-    pub fn is_would_block(&self) -> bool {
-        match *self {
-            StreamErr::IoError(ref err) => err.kind() == io::ErrorKind::WouldBlock,
-            _ => false,
-        }
-    }
-
-    pub fn description(&self) -> &str {
-        use self::StreamErr::*;
-        match *self {
-            IoError(ref io_err) => io_err.description(),
-            CantResolveAddr => "Can't resolve address",
-            TlsError(ref tls_err) => tls_err.description(),
-            ConnectionClosed => "Connection closed",
-        }
-    }
-
-    pub fn source(&self) -> Option<&dyn error::Error> {
-        use self::StreamErr::*;
-        match *self {
-            IoError(ref err) => err.source(),
-            CantResolveAddr => None,
-            TlsError(ref err) => err.source(),
-            ConnectionClosed => None,
-        }
-    }
-}
-
-pub type Result<T> = result::Result<T, StreamErr>;
-
-impl From<TcpError> for StreamErr {
-    fn from(tcp_err: TcpError) -> StreamErr {
-        match tcp_err {
-            TcpError::IoError(io_err) => StreamErr::IoError(io_err),
-            TcpError::CantResolveAddr => StreamErr::CantResolveAddr,
-            TcpError::ConnectionClosed => StreamErr::ConnectionClosed,
-        }
-    }
-}
-
-impl From<TlsError> for StreamErr {
-    fn from(tls_err: TlsError) -> StreamErr {
-        match tls_err {
-            TlsError::TcpError(err) => StreamErr::from(err),
-            TlsError::TlsError(err) => StreamErr::TlsError(err),
-        }
-    }
-}
-
-impl From<IoError> for StreamErr {
-    fn from(io_err: IoError) -> StreamErr {
-        StreamErr::IoError(io_err)
-    }
-}
-
-impl<'poll> Stream<'poll> {
-    pub fn new(
-        poll: &'poll Poll,
-        serv_addr: &str,
-        serv_port: u16,
-        tls: bool,
-    ) -> Result<Stream<'poll>> {
-        if tls {
-            TlsStream::new(poll, serv_addr, serv_port)
-                .map_err(StreamErr::from)
-                .map(Stream::Tls)
-        } else {
-            TcpStream::new(poll, serv_addr, serv_port)
-                .map_err(StreamErr::from)
-                .map(Stream::Tcp)
-        }
-    }
-
-    /// NOTE: Calling this on a broken/closed stream causes a panic! See #120.
-    pub fn write_ready(&mut self) -> Result<()> {
-        match *self {
-            Stream::Tcp(ref mut s) => s.write_ready().map_err(StreamErr::from),
-            Stream::Tls(ref mut s) => s.write_ready().map_err(StreamErr::from),
-        }
-    }
-
-    /// NOTE: Calling this on a broken/closed stream causes a panic! See #120.
-    pub fn read_ready(&mut self, buf: &mut [u8]) -> Result<usize> {
-        match *self {
-            Stream::Tcp(ref mut s) => s.read_ready(buf).map_err(StreamErr::from),
-            Stream::Tls(ref mut s) => s.read_ready(buf).map_err(StreamErr::from),
-        }
-    }
-
-    /// NOTE: Calling this on a broken/closed stream causes a panic! See #120.
-    pub fn get_tok(&self) -> Token {
-        match *self {
-            Stream::Tcp(ref s) => s.get_tok(),
-            Stream::Tls(ref s) => s.get_tok(),
-        }
-    }
-}
-
-impl<'poll> Write for Stream<'poll> {
-    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        match *self {
-            Stream::Tcp(ref mut s) => s.write(buf),
-            Stream::Tls(ref mut s) => s.write(buf),
-        }
-    }
-
-    fn flush(&mut self) -> io::Result<()> {
-        match *self {
-            Stream::Tcp(ref mut s) => s.flush(),
-            Stream::Tls(ref mut s) => s.flush(),
-        }
-    }
-}
diff --git a/src/stream/tcp.rs b/src/stream/tcp.rs
deleted file mode 100644
index 0fc1a609..00000000
--- a/src/stream/tcp.rs
+++ /dev/null
@@ -1,123 +0,0 @@
-use crate::stream::utils::*;
-use mio::Poll;
-use mio::Token;
-use net2::TcpBuilder;
-use net2::TcpStreamExt;
-use std::io;
-use std::io::Read;
-use std::io::Write;
-use std::net;
-use std::net::SocketAddr;
-use std::net::ToSocketAddrs;
-use std::os::unix::io::AsRawFd;
-use std::result::Result;
-
-/// A buffered TCP stream that registers itself for readiness events when the buffer is not empty.
-/// Use `Write` instance for writing to the buffer. Use `Read` instance when the stream is ready
-/// for reading. Use `send` when the stream is ready for writing.
-pub struct TcpStream<'poll> {
-    inner: net::TcpStream,
-    poll: &'poll Poll,
-    out_buf: Vec<u8>,
-}
-
-#[derive(Debug)]
-pub enum TcpError {
-    IoError(io::Error),
-    CantResolveAddr,
-    ConnectionClosed,
-}
-
-impl<'poll> TcpStream<'poll> {
-    pub fn new(
-        poll: &'poll Poll,
-        serv_addr: &str,
-        serv_port: u16,
-    ) -> Result<TcpStream<'poll>, TcpError> {
-        let mut addr_iter = (serv_addr, serv_port)
-            .to_socket_addrs()
-            .map_err(TcpError::IoError)?;
-        let addr = addr_iter.next().ok_or(TcpError::CantResolveAddr)?;
-        let stream = {
-            match addr {
-                SocketAddr::V4(_) => TcpBuilder::new_v4()
-                    .map_err(TcpError::IoError)?
-                    .to_tcp_stream()
-                    .map_err(TcpError::IoError)?,
-                SocketAddr::V6(_) => TcpBuilder::new_v6()
-                    .map_err(TcpError::IoError)?
-                    .to_tcp_stream()
-                    .map_err(TcpError::IoError)?,
-            }
-        };
-        stream.set_nonblocking(true).map_err(TcpError::IoError)?;
-        // This will fail with EINPROGRESS
-        let _ = stream.connect(addr);
-        register_for_r(poll, stream.as_raw_fd());
-        Ok(TcpStream {
-            inner: stream,
-            poll,
-            out_buf: Vec::with_capacity(1024),
-        })
-    }
-
-    /// Call when the stream is ready for writing.
-    pub fn write_ready(&mut self) -> io::Result<()> {
-        let to_send = self.out_buf.len();
-        match self.inner.write(&self.out_buf) {
-            Ok(bytes_sent) => {
-                self.out_buf.drain(0..bytes_sent);
-                let register = if bytes_sent == to_send {
-                    reregister_for_r
-                } else {
-                    reregister_for_rw
-                };
-                register(&self.poll, self.inner.as_raw_fd());
-                Ok(())
-            }
-            Err(err) => {
-                reregister_for_rw(&self.poll, self.inner.as_raw_fd());
-                Err(err)
-            }
-        }
-    }
-
-    /// Call when the stream is ready for writing.
-    pub fn read_ready(&mut self, buf: &mut [u8]) -> Result<usize, TcpError> {
-        match self.read(buf) {
-            Ok(0) => Err(TcpError::ConnectionClosed),
-            Ok(n) => Ok(n),
-            Err(err) => Err(TcpError::IoError(err)),
-        }
-    }
-
-    pub fn get_tok(&self) -> Token {
-        Token(self.inner.as_raw_fd() as usize)
-    }
-}
-
-// Drop just deregisters the socket
-impl<'poll> Drop for TcpStream<'poll> {
-    fn drop(&mut self) {
-        deregister(&self.poll, self.inner.as_raw_fd());
-    }
-}
-
-impl<'poll> Read for TcpStream<'poll> {
-    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
-        self.inner.read(buf)
-    }
-}
-
-impl<'poll> Write for TcpStream<'poll> {
-    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        // TODO inefficient when the socket is already ready for writing
-        self.out_buf.extend(buf);
-        reregister_for_rw(&self.poll, self.inner.as_raw_fd());
-        Ok(buf.len())
-    }
-
-    fn flush(&mut self) -> io::Result<()> {
-        Ok(())
-    }
-}
diff --git a/src/stream/tls.rs b/src/stream/tls.rs
deleted file mode 100644
index 16881866..00000000
--- a/src/stream/tls.rs
+++ /dev/null
@@ -1,121 +0,0 @@
-use crate::stream::tcp::{TcpError, TcpStream};
-use mio::Poll;
-use mio::Token;
-use native_tls as tls;
-use std::io;
-use std::io::Read;
-use std::io::Write;
-use std::result::Result;
-use take_mut::take;
-
-pub enum TlsStream<'poll> {
-    Handshake {
-        stream: tls::MidHandshakeTlsStream<TcpStream<'poll>>,
-        // send buffer to be able to provide `Write` impl
-        out_buf: Vec<u8>,
-    },
-    Connected {
-        stream: tls::TlsStream<TcpStream<'poll>>,
-    },
-    Broken,
-}
-
-#[derive(Debug)]
-pub enum TlsError {
-    TcpError(TcpError),
-    TlsError(tls::Error),
-}
-
-impl<'poll> TlsStream<'poll> {
-    pub fn new(
-        poll: &'poll Poll,
-        serv_addr: &str,
-        serv_port: u16,
-    ) -> Result<TlsStream<'poll>, TlsError> {
-        let connector = tls::TlsConnector::builder()
-            .build()
-            .map_err(TlsError::TlsError)?;
-        let tcp_stream = TcpStream::new(poll, serv_addr, serv_port).map_err(TlsError::TcpError)?;
-        match connector.connect(serv_addr, tcp_stream) {
-            Ok(tls_stream) => Ok(TlsStream::Connected { stream: tls_stream }),
-            Err(tls::HandshakeError::WouldBlock(mid)) => Ok(TlsStream::Handshake {
-                stream: mid,
-                out_buf: Vec::with_capacity(1024),
-            }),
-            Err(tls::HandshakeError::Failure(err)) => Err(TlsError::TlsError(err)),
-        }
-    }
-
-    pub fn write_ready(&mut self) -> io::Result<()> {
-        match *self {
-            TlsStream::Handshake { ref mut stream, .. } => stream.get_mut().write_ready(),
-            TlsStream::Connected { ref mut stream } => stream.get_mut().write_ready(),
-            TlsStream::Broken => panic!("write_ready() called on broken tls stream"),
-        }
-    }
-
-    pub fn read_ready(&mut self, buf: &mut [u8]) -> Result<usize, TlsError> {
-        let mut ret = Ok(0);
-        take(self, |s| match s {
-            TlsStream::Handshake { stream, out_buf } => match stream.handshake() {
-                Ok(mut tls_stream) => {
-                    ret = tls_stream
-                        .write(&out_buf)
-                        .map_err(|err| TlsError::TcpError(TcpError::IoError(err)));
-                    TlsStream::Connected { stream: tls_stream }
-                }
-                Err(tls::HandshakeError::WouldBlock(mid)) => TlsStream::Handshake {
-                    stream: mid,
-                    out_buf,
-                },
-                Err(tls::HandshakeError::Failure(err)) => {
-                    ret = Err(TlsError::TlsError(err));
-                    TlsStream::Broken
-                }
-            },
-            TlsStream::Connected { mut stream } => {
-                match stream.read(buf) {
-                    Ok(0) => {
-                        ret = Err(TlsError::TcpError(TcpError::ConnectionClosed));
-                    }
-                    Ok(n) => {
-                        ret = Ok(n);
-                    }
-                    Err(err) => {
-                        ret = Err(TlsError::TcpError(TcpError::IoError(err)));
-                    }
-                }
-                TlsStream::Connected { stream }
-            }
-            TlsStream::Broken => panic!("read_ready() called on broken tls stream"),
-        });
-        ret
-    }
-
-    pub fn get_tok(&self) -> Token {
-        match *self {
-            TlsStream::Handshake { ref stream, .. } => stream.get_ref().get_tok(),
-            TlsStream::Connected { ref stream } => stream.get_ref().get_tok(),
-            TlsStream::Broken => panic!("get_tok() called on broken tls stream"),
-        }
-    }
-}
-
-impl<'poll> Write for TlsStream<'poll> {
-    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
-        match *self {
-            TlsStream::Handshake {
-                ref mut out_buf, ..
-            } => {
-                out_buf.extend(buf);
-                Ok(buf.len())
-            }
-            TlsStream::Connected { ref mut stream } => stream.write(buf),
-            TlsStream::Broken => Ok(0),
-        }
-    }
-
-    fn flush(&mut self) -> io::Result<()> {
-        Ok(())
-    }
-}
diff --git a/src/stream/utils.rs b/src/stream/utils.rs
deleted file mode 100644
index c2e253ea..00000000
--- a/src/stream/utils.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use mio::unix::EventedFd;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
-use std::os::unix::io::RawFd;
-
-pub fn register_for_r(poll: &Poll, fd: RawFd) {
-    let _ = poll.register(
-        &EventedFd(&fd),
-        Token(fd as usize),
-        Ready::readable(),
-        PollOpt::level(),
-    );
-}
-
-pub fn reregister_for_r(poll: &Poll, fd: RawFd) {
-    let _ = poll.reregister(
-        &EventedFd(&fd),
-        Token(fd as usize),
-        Ready::readable(),
-        PollOpt::level(),
-    );
-}
-
-pub fn reregister_for_rw(poll: &Poll, fd: RawFd) {
-    let _ = poll.reregister(
-        &EventedFd(&fd),
-        Token(fd as usize),
-        Ready::readable() | Ready::writable(),
-        PollOpt::level(),
-    );
-}
-
-pub fn deregister(poll: &Poll, fd: RawFd) {
-    let _ = poll.deregister(&EventedFd(&fd));
-}
diff --git a/src/tui/tab.rs b/src/tui/tab.rs
deleted file mode 100644
index dfb26edb..00000000
--- a/src/tui/tab.rs
+++ /dev/null
@@ -1,87 +0,0 @@
-use termbox_simple::{Termbox, TB_UNDERLINE};
-
-use crate::config::Colors;
-use crate::config::Style;
-use crate::notifier::Notifier;
-use crate::tui::messaging::MessagingUI;
-use crate::tui::MsgSource;
-
-pub struct Tab {
-    pub widget: MessagingUI,
-    pub src: MsgSource,
-    pub style: TabStyle,
-    /// Alt-character to use to switch to this tab.
-    pub switch: Option<char>,
-    pub notifier: Notifier,
-}
-
-// NOTE: Keep the variants sorted in increasing significance, to avoid updating
-// style with higher significance for a less significant style (e.g. updating
-// from `Highlight` to `NewMsg` in `set_tab_style`).
-#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
-pub enum TabStyle {
-    Normal,
-    NewMsg,
-    Highlight,
-}
-
-impl TabStyle {
-    pub fn get_style(self, colors: &Colors) -> Style {
-        match self {
-            TabStyle::Normal => colors.tab_normal,
-            TabStyle::NewMsg => colors.tab_new_msg,
-            TabStyle::Highlight => colors.tab_highlight,
-        }
-    }
-}
-
-impl Tab {
-    pub fn visible_name(&self) -> &str {
-        self.src.visible_name()
-    }
-
-    pub fn set_style(&mut self, style: TabStyle) {
-        self.style = style;
-    }
-
-    pub fn update_source<F>(&mut self, f: &F)
-    where
-        F: Fn(&mut MsgSource),
-    {
-        f(&mut self.src)
-    }
-
-    pub fn width(&self) -> i32 {
-        // TODO: assuming ASCII string here. We should probably switch to a AsciiStr type.
-        self.visible_name().len() as i32 + if self.widget.get_ignore_state() { 0 } else { 3 }
-    }
-
-    pub fn draw(
-        &self,
-        tb: &mut Termbox,
-        colors: &Colors,
-        mut pos_x: i32,
-        pos_y: i32,
-        active: bool,
-    ) {
-        let style: Style = if active {
-            colors.tab_active
-        } else {
-            self.style.get_style(colors)
-        };
-
-        let mut switch_drawn = false;
-        for ch in self.visible_name().chars() {
-            if Some(ch) == self.switch && !switch_drawn {
-                tb.change_cell(pos_x, pos_y, ch, style.fg | TB_UNDERLINE, style.bg);
-                switch_drawn = true;
-            } else {
-                tb.change_cell(pos_x, pos_y, ch, style.fg, style.bg);
-            }
-            pos_x += 1;
-        }
-        if !self.widget.get_ignore_state() {
-            crate::tui::termbox::print_chars(tb, pos_x, pos_y, style, "[i]".chars());
-        }
-    }
-}
diff --git a/src/tui/termbox.rs b/src/tui/termbox.rs
deleted file mode 100644
index 263437ea..00000000
--- a/src/tui/termbox.rs
+++ /dev/null
@@ -1,21 +0,0 @@
-//! Some utilities for termbox
-
-use crate::config::Style;
-use termbox_simple::Termbox;
-
-pub fn print(tb: &mut Termbox, mut pos_x: i32, pos_y: i32, style: Style, str: &str) {
-    for char in str.chars() {
-        tb.change_cell(pos_x, pos_y, char, style.fg, style.bg);
-        pos_x += 1;
-    }
-}
-
-pub fn print_chars<C>(tb: &mut Termbox, mut pos_x: i32, pos_y: i32, style: Style, chars: C)
-where
-    C: Iterator<Item = char>,
-{
-    for char in chars {
-        tb.change_cell(pos_x, pos_y, char, style.fg, style.bg);
-        pos_x += 1;
-    }
-}
diff --git a/src/ui.rs b/src/ui.rs
new file mode 100644
index 00000000..569c662e
--- /dev/null
+++ b/src/ui.rs
@@ -0,0 +1,159 @@
+// I think borrowed boxing is necessary for objekt::clone_box to work
+#![allow(clippy::borrowed_box)]
+
+//! UI event handling
+
+use crate::cmd::{parse_cmd, CmdArgs, ParseCmdResult};
+use crate::config;
+use futures_util::stream::StreamExt;
+use libtiny_client::Client;
+use libtiny_ui::{MsgSource, MsgTarget, UI};
+use std::path::{Path, PathBuf};
+use tokio::sync::mpsc;
+
+pub(crate) async fn task(
+    config_path: PathBuf,
+    defaults: config::Defaults,
+    ui: Box<dyn UI>,
+    mut clients: Vec<Client>,
+    mut rcv_ev: mpsc::Receiver<libtiny_ui::Event>,
+) {
+    while let Some(ev) = rcv_ev.next().await {
+        if handle_input_ev(&config_path, &defaults, &ui, &mut clients, ev) {
+            return;
+        }
+        ui.draw();
+    }
+}
+
+fn handle_input_ev(
+    config_path: &Path,
+    defaults: &config::Defaults,
+    ui: &Box<dyn UI>,
+    clients: &mut Vec<Client>,
+    ev: libtiny_ui::Event,
+) -> bool {
+    use libtiny_ui::Event::*;
+    match ev {
+        Abort => {
+            for client in clients {
+                client.quit(None);
+            }
+            return true; // abort
+        }
+        Msg { msg, source } => {
+            send_msg(&**ui, clients, &source, msg, false);
+        }
+        Lines { lines, source } => {
+            for line in lines.into_iter() {
+                send_msg(&**ui, clients, &source, line, false)
+            }
+        }
+        Cmd { cmd, source } => handle_cmd(config_path, defaults, ui, clients, source, &cmd),
+    }
+
+    false // continue
+}
+
+fn handle_cmd(
+    config_path: &Path,
+    defaults: &config::Defaults,
+    ui: &Box<dyn UI>,
+    clients: &mut Vec<Client>,
+    src: MsgSource,
+    cmd: &str,
+) {
+    match parse_cmd(cmd) {
+        ParseCmdResult::Ok { cmd, rest } => {
+            let cmd_args = CmdArgs {
+                args: rest,
+                config_path,
+                defaults,
+                ui,
+                clients,
+                src,
+            };
+            (cmd.cmd_fn)(cmd_args);
+        }
+        // ParseCmdResult::Ambiguous(vec) => {
+        //     self.ui.add_client_err_msg(
+        //         &format!("Unsupported command: \"/{}\"", msg),
+        //         &MsgTarget::CurrentTab,
+        //     );
+        //     self.ui.add_client_err_msg(
+        //         &format!("Did you mean one of {:?} ?", vec),
+        //         &MsgTarget::CurrentTab,
+        //     );
+        // },
+        ParseCmdResult::Unknown => ui.add_client_err_msg(
+            &format!("Unsupported command: \"/{}\"", cmd),
+            &MsgTarget::CurrentTab,
+        ),
+    }
+}
+
+// TODO: move this somewhere else
+pub(crate) fn send_msg(
+    ui: &dyn UI,
+    clients: &mut Vec<Client>,
+    src: &MsgSource,
+    msg: String,
+    is_action: bool,
+) {
+    if src.serv_name() == "mentions" {
+        ui.add_client_err_msg(
+            "Use `/connect <server>` to connect to a server",
+            &MsgTarget::CurrentTab,
+        );
+        return;
+    }
+
+    let client = clients
+        .iter_mut()
+        .find(|client| client.get_serv_name() == src.serv_name())
+        .unwrap();
+
+    // TODO: For errors:
+    //
+    // ui.add_client_err_msg(
+    //     &format!("Can't find server: {}", serv),
+    //     &MsgTarget::CurrentTab,
+    // );
+
+    // `ui_target`: Where to show the message on ui
+    // `msg_target`: Actual PRIVMSG target to send to the server
+    let (ui_target, msg_target) = {
+        match src {
+            MsgSource::Serv { .. } => {
+                // we don't split raw messages to 512-bytes long chunks
+                client.raw_msg(&msg);
+                return;
+            }
+
+            MsgSource::Chan { ref serv, ref chan } => (MsgTarget::Chan { serv, chan }, chan),
+
+            MsgSource::User { ref serv, ref nick } => {
+                let msg_target = if nick.eq_ignore_ascii_case("nickserv")
+                    || nick.eq_ignore_ascii_case("chanserv")
+                {
+                    MsgTarget::Server { serv }
+                } else {
+                    MsgTarget::User { serv, nick }
+                };
+                (msg_target, nick)
+            }
+        }
+    };
+
+    let ts = time::now();
+    let extra_len = msg_target.len()
+        + if is_action {
+            9 // "\0x1ACTION \0x1".len()
+        } else {
+            0
+        };
+    for msg in client.split_privmsg(extra_len, &msg) {
+        client.privmsg(msg_target, msg, is_action);
+        ui.add_privmsg(&client.get_nick(), msg, ts, &ui_target, false, is_action);
+    }
+}
diff --git a/src/utils.rs b/src/utils.rs
index 62b48cc9..ffb1d036 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -1,46 +1,7 @@
-pub struct InsertIterator<'iter, A: 'iter> {
-    insert_point: usize,
-    current_idx: usize,
-    iter_orig: &'iter mut dyn Iterator<Item = A>,
-    iter_insert: &'iter mut dyn Iterator<Item = A>,
-}
-
-impl<'iter, A> Iterator for InsertIterator<'iter, A> {
-    type Item = A;
-
-    fn next(&mut self) -> Option<A> {
-        if self.current_idx >= self.insert_point {
-            if let Some(a) = self.iter_insert.next() {
-                Some(a)
-            } else {
-                self.iter_orig.next()
-            }
-        } else {
-            self.current_idx += 1;
-            self.iter_orig.next()
-        }
-    }
-}
-
-pub fn insert_iter<'iter, A>(
-    iter_orig: &'iter mut dyn Iterator<Item = A>,
-    iter_insert: &'iter mut dyn Iterator<Item = A>,
-    insert_point: usize,
-) -> InsertIterator<'iter, A> {
-    InsertIterator {
-        insert_point,
-        current_idx: 0,
-        iter_orig,
-        iter_insert,
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
 use std::str::SplitWhitespace;
 
 /// Like `std::str::SplitWhitespace`, but returns beginning indices rather than slices.
-pub struct SplitWhitespaceIndices<'a> {
+pub(crate) struct SplitWhitespaceIndices<'a> {
     inner: SplitWhitespace<'a>,
     str: &'a str,
 }
@@ -55,7 +16,7 @@ impl<'a> Iterator for SplitWhitespaceIndices<'a> {
     }
 }
 
-pub fn split_whitespace_indices(str: &str) -> SplitWhitespaceIndices {
+pub(crate) fn split_whitespace_indices(str: &str) -> SplitWhitespaceIndices {
     SplitWhitespaceIndices {
         inner: str.split_whitespace(),
         str,
@@ -64,74 +25,7 @@ pub fn split_whitespace_indices(str: &str) -> SplitWhitespaceIndices {
 
 ////////////////////////////////////////////////////////////////////////////////
 
-pub struct SplitIterator<'a> {
-    s: Option<&'a str>,
-    max: usize,
-}
-
-/// Iterate over subslices that are at most `max` long (in bytes). Splits are
-/// made on whitespace characters when possible.
-pub fn split_iterator(s: &str, max: usize) -> SplitIterator {
-    SplitIterator { s: Some(s), max }
-}
-
-impl<'a> Iterator for SplitIterator<'a> {
-    type Item = &'a str;
-
-    fn next(&mut self) -> Option<&'a str> {
-        if self.max == 0 {
-            return None;
-        }
-
-        match self.s {
-            None => None,
-            Some(s) => {
-                if s.len() <= self.max {
-                    let ret = Some(s);
-                    self.s = None;
-                    ret
-                } else {
-                    let mut split = 0;
-
-                    // try to split at a whitespace character
-                    for (ws_idx, ws_char) in s.rmatch_indices(char::is_whitespace) {
-                        if ws_idx <= self.max {
-                            // should we include ws char?
-                            if ws_idx + ws_char.len() <= self.max {
-                                split = ws_idx + ws_char.len();
-                            } else {
-                                split = ws_idx;
-                            }
-                            break;
-                        }
-                    }
-
-                    if split == 0 {
-                        // couldn't split at a whitespace, just split at any char
-                        for i in 0..4 {
-                            if s.is_char_boundary(self.max - i) {
-                                split = self.max - i;
-                                break;
-                            }
-                        }
-                    }
-
-                    if split == 0 {
-                        panic!("Can't split long msg: {:?}", s);
-                    } else {
-                        let ret = Some(&s[0..split]);
-                        self.s = Some(&s[split..]);
-                        ret
-                    }
-                }
-            }
-        }
-    }
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
-pub fn is_nick_char(c: char) -> bool {
+pub(crate) fn is_nick_char(c: char) -> bool {
     // from RFC 2812:
     //
     // nickname   =  ( letter / special ) *8( letter / digit / special / "-" )
@@ -149,231 +43,25 @@ pub fn is_nick_char(c: char) -> bool {
 
 ////////////////////////////////////////////////////////////////////////////////
 
-use std::iter::Peekable;
-use std::str::Chars;
-
-/// Parse at least one, at most two digits. Does not consume the iterator when
-/// result is `None`.
-fn parse_color_code(chars: &mut Peekable<Chars>) -> Option<u8> {
-    fn to_dec(ch: char) -> Option<u8> {
-        ch.to_digit(10).map(|c| c as u8)
-    }
-
-    let c1_char = *chars.peek()?;
-    let c1_digit = match to_dec(c1_char) {
-        None => {
-            return None;
-        }
-        Some(c1_digit) => {
-            chars.next();
-            c1_digit
-        }
-    };
-
-    match chars.peek().cloned() {
-        None => Some(c1_digit),
-        Some(c2) => match to_dec(c2) {
-            None => Some(c1_digit),
-            Some(c2_digit) => {
-                chars.next();
-                Some(c1_digit * 10 + c2_digit)
-            }
-        },
-    }
-}
-
-/// Translate IRC color codes using the callback, and remove ASCII control chars from the input.
-pub fn translate_irc_control_chars(
-    str: &str,
-    push_color: fn(ret: &mut String, fg: u8, bg: Option<u8>),
-) -> String {
-    let mut ret = String::with_capacity(str.len());
-    let mut iter = str.chars().peekable();
-
-    while let Some(char) = iter.next() {
-        if char == '\x03' {
-            match parse_color_code(&mut iter) {
-                None => {
-                    // just skip the control char
-                }
-                Some(fg) => {
-                    if let Some(char) = iter.peek().cloned() {
-                        if char == ',' {
-                            iter.next(); // consume ','
-                            match parse_color_code(&mut iter) {
-                                None => {
-                                    // comma was not part of the color code,
-                                    // add it to the new string
-                                    push_color(&mut ret, fg, None);
-                                    ret.push(char);
-                                }
-                                Some(bg) => {
-                                    push_color(&mut ret, fg, Some(bg));
-                                }
-                            }
-                        } else {
-                            push_color(&mut ret, fg, None);
-                        }
-                    } else {
-                        push_color(&mut ret, fg, None);
-                    }
-                }
-            }
-        } else if !char.is_ascii_control() {
-            ret.push(char);
-        }
-    }
-
-    ret
-}
-
-/// Like `translate_irc_control_chars`, but skips color codes.
-pub fn remove_irc_control_chars(str: &str) -> String {
-    fn push_color(_ret: &mut String, _fg: u8, _bg: Option<u8>) {}
-    translate_irc_control_chars(str, push_color)
-}
-
-////////////////////////////////////////////////////////////////////////////////
-
 #[cfg(test)]
 mod tests {
 
-    extern crate test;
-
     use super::*;
-    use quickcheck::QuickCheck;
-
-    #[test]
-    fn insert_iter_test() {
-        let mut range1 = 0..5;
-        let mut range2 = 5..10;
-        let iter = insert_iter(&mut range1, &mut range2, 3);
-        assert_eq!(
-            iter.collect::<Vec<i32>>(),
-            vec![0, 1, 2, 5, 6, 7, 8, 9, 3, 4]
-        )
-    }
 
     #[test]
     fn split_ws_idx() {
         let str = "x y z";
-        let idxs: Vec<usize> = split_whitespace_indices(str).into_iter().collect();
+        let idxs: Vec<usize> = split_whitespace_indices(str).collect();
         assert_eq!(idxs, vec![0, 2, 4]);
 
         let str = "       ";
-        let idxs: Vec<usize> = split_whitespace_indices(str).into_iter().collect();
+        let idxs: Vec<usize> = split_whitespace_indices(str).collect();
         let expected: Vec<usize> = vec![];
         // Next line fails when I inline `expected`. Probably a rustc bug.
         assert_eq!(idxs, expected);
 
         let str = "  foo    bar  \n\r   baz     ";
-        let idxs: Vec<usize> = split_whitespace_indices(str).into_iter().collect();
+        let idxs: Vec<usize> = split_whitespace_indices(str).collect();
         assert_eq!(idxs, vec![2, 9, 19]);
     }
-
-    #[test]
-    fn test_split_iterator_1() {
-        let iter = split_iterator("yada yada yada", 5);
-        assert_eq!(
-            iter.into_iter().collect::<Vec<&str>>(),
-            vec!["yada ", "yada ", "yada"]
-        );
-    }
-
-    #[test]
-    fn test_split_iterator_2() {
-        let iter = split_iterator("yada yada yada", 4);
-        assert_eq!(
-            iter.into_iter().collect::<Vec<&str>>(),
-            // weird but OK
-            vec!["yada", " ", "yada", " ", "yada"]
-        );
-    }
-
-    #[test]
-    fn test_split_iterator_3() {
-        let iter = split_iterator("yada yada yada", 3);
-        assert_eq!(
-            iter.into_iter().collect::<Vec<&str>>(),
-            vec!["yad", "a ", "yad", "a ", "yad", "a"]
-        );
-    }
-
-    #[test]
-    fn test_split_iterator_4() {
-        let iter = split_iterator("longwordislong", 3);
-        assert_eq!(
-            iter.into_iter().collect::<Vec<&str>>(),
-            vec!["lon", "gwo", "rdi", "slo", "ng"]
-        );
-    }
-
-    #[test]
-    fn test_split_iterator_5() {
-        let iter = split_iterator("", 3);
-        assert_eq!(iter.into_iter().collect::<Vec<&str>>(), vec![""]);
-    }
-
-    #[test]
-    fn test_split_iterator_6() {
-        let iter = split_iterator("", 0);
-        let ret: Vec<&str> = vec![];
-        assert_eq!(iter.into_iter().collect::<Vec<&str>>(), ret);
-    }
-
-    #[test]
-    fn split_iterator_prop_1() {
-        fn prop(s: String, max: u8) -> bool {
-            // at least one character shoudl fit into the buffer
-            if max < 4 {
-                return true;
-            }
-            // println!("trying s: {}, max: {}", s, max);
-            for slice in split_iterator(&s, max as usize) {
-                if slice.len() > max as usize {
-                    return false;
-                }
-            }
-            return true;
-        }
-
-        QuickCheck::new()
-            .tests(1000)
-            .quickcheck(prop as fn(String, u8) -> bool);
-    }
-
-    #[test]
-    fn split_iterator_prop_2() {
-        fn prop(s: String, max: u8) -> bool {
-            if max < 4 {
-                return true;
-            }
-            let len: usize = split_iterator(&s, max as usize).map(str::len).sum();
-            len == s.len()
-        }
-
-        QuickCheck::new()
-            .tests(1000)
-            .quickcheck(prop as fn(String, u8) -> bool);
-    }
-
-    #[test]
-    fn test_parse_color_code() {
-        assert_eq!(parse_color_code(&mut "1".chars().peekable()), Some(1));
-        assert_eq!(parse_color_code(&mut "01".chars().peekable()), Some(1));
-        assert_eq!(parse_color_code(&mut "1,".chars().peekable()), Some(1));
-    }
-
-    #[test]
-    fn test_translate_irc_control_chars() {
-        assert_eq!(
-            remove_irc_control_chars("  Le Voyageur imprudent  "),
-            "  Le Voyageur imprudent  "
-        );
-        assert_eq!(remove_irc_control_chars("\x0301,02foo"), "foo");
-        assert_eq!(remove_irc_control_chars("\x0301,2foo"), "foo");
-        assert_eq!(remove_irc_control_chars("\x031,2foo"), "foo");
-        assert_eq!(remove_irc_control_chars("\x031,foo"), ",foo");
-        assert_eq!(remove_irc_control_chars("\x03,foo"), ",foo");
-    }
 }
diff --git a/src/wire.rs b/src/wire.rs
deleted file mode 100644
index 634fec4a..00000000
--- a/src/wire.rs
+++ /dev/null
@@ -1,590 +0,0 @@
-//! IRC wire protocol message parsers and generators. Incomplete; new messages are added as needed.
-
-use std;
-use std::io::Write;
-use std::str;
-
-use crate::logger::LogFile;
-
-pub fn pass<W: Write>(sink: &mut W, pass: &str) -> std::io::Result<()> {
-    write!(sink, "PASS {}\r\n", pass)
-}
-
-pub fn user<W: Write>(sink: &mut W, hostname: &str, realname: &str) -> std::io::Result<()> {
-    write!(sink, "USER {} 8 * :{}\r\n", hostname, realname)
-}
-
-pub fn nick<W: Write>(sink: &mut W, arg: &str) -> std::io::Result<()> {
-    write!(sink, "NICK {}\r\n", arg)
-}
-
-pub fn ping<W: Write>(sink: &mut W, arg: &str) -> std::io::Result<()> {
-    write!(sink, "PING {}\r\n", arg)
-}
-
-pub fn pong<W: Write>(sink: &mut W, arg: &str) -> std::io::Result<()> {
-    write!(sink, "PONG {}\r\n", arg)
-}
-
-pub fn join<W: Write>(sink: &mut W, chans: &[&str]) -> std::io::Result<()> {
-    write!(sink, "JOIN {}\r\n", chans.join(","))
-}
-
-pub fn part<W: Write>(sink: &mut W, channel: &str) -> std::io::Result<()> {
-    write!(sink, "PART {}\r\n", channel)
-}
-
-pub fn privmsg<W: Write>(sink: &mut W, msgtarget: &str, msg: &str) -> std::io::Result<()> {
-    // IRC messages need to be shorter than 512 bytes (see RFC 1459 or 2812). This should be dealt
-    // with at call sites as we can't show how we split messages into multiple messages in the UI
-    // at this point.
-    assert!(msgtarget.len() + msg.len() + 12 <= 512);
-    write!(sink, "PRIVMSG {} :{}\r\n", msgtarget, msg)
-}
-
-pub fn ctcp_action<W: Write>(sink: &mut W, msgtarget: &str, msg: &str) -> std::io::Result<()> {
-    assert!(msgtarget.len() + msg.len() + 21 <= 512); // See comments in `privmsg`
-    write!(sink, "PRIVMSG {} :\x01ACTION {}\x01\r\n", msgtarget, msg)
-}
-
-pub fn away<W: Write>(sink: &mut W, msg: Option<&str>) -> std::io::Result<()> {
-    match msg {
-        None => write!(sink, "AWAY\r\n"),
-        Some(msg) => write!(sink, "AWAY :{}\r\n", msg),
-    }
-}
-
-pub fn cap_ls<W: Write>(sink: &mut W) -> std::io::Result<()> {
-    write!(sink, "CAP LS\r\n")
-}
-
-pub fn cap_req<W: Write>(sink: &mut W, cap_identifiers: &[&str]) -> std::io::Result<()> {
-    write!(sink, "CAP REQ :{}\r\n", cap_identifiers.join(" "))
-}
-
-pub fn cap_end<W: Write>(sink: &mut W) -> std::io::Result<()> {
-    write!(sink, "CAP END\r\n")
-}
-
-pub fn authenticate<W: Write>(sink: &mut W, msg: &str) -> std::io::Result<()> {
-    write!(sink, "AUTHENTICATE {}\r\n", msg)
-}
-
-/*
-pub fn quit<W : Write>(mut sink: W, msg : Option<&str>) -> std::io::Result<()> {
-    match msg {
-        None => write!(sink, "QUIT\r\n"),
-        Some(msg) => write!(sink, "QUIT {}\r\n", msg)
-    }
-}
-*/
-
-/// <prefix> ::= <servername> | <nick> [ '!' <user> ] [ '@' <host> ]
-/// From RFC 2812:
-/// > If the prefix is missing from the message, it is assumed to have originated from the
-/// > connection from which it was received from.
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum Pfx {
-    Server(String),
-
-    /// <nick>!<user>@<host>
-    User {
-        nick: String,
-        /// user@host
-        user: String,
-    },
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum MsgTarget {
-    Chan(String),
-    User(String),
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub struct Msg {
-    pub pfx: Option<Pfx>,
-    pub cmd: Cmd,
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum Cmd {
-    /// A PRIVMSG or NOTICE. Check `is_notice` field.
-    PRIVMSG {
-        // TODO: In theory this should be a list of targets, but in practice I've never
-        // encountered that case.
-        target: MsgTarget,
-        msg: String,
-        is_notice: bool,
-    },
-
-    JOIN {
-        // TODO: Same as above, this should be a list ...
-        chan: String, // TODO: key field might be useful when joining restricted channels. In practice I've never
-                      // needed it.
-    },
-
-    PART {
-        // TODO: List of channels
-        chan: String,
-        msg: Option<String>,
-    },
-
-    QUIT {
-        msg: Option<String>,
-    },
-
-    NICK {
-        nick: String,
-    },
-
-    PING {
-        server: String,
-    },
-
-    PONG {
-        server: String,
-    },
-
-    ERROR {
-        msg: String,
-    },
-
-    TOPIC {
-        chan: String,
-        topic: String,
-    },
-
-    CAP {
-        client: String,
-        subcommand: String,
-        params: Vec<String>,
-    },
-
-    AUTHENTICATE {
-        param: String,
-    },
-
-    /// An IRC message other than the ones listed above.
-    Other {
-        cmd: String,
-        params: Vec<String>,
-    },
-
-    /// Numeric replies are kept generic as there are just too many replies and we probably only
-    /// need to handle a small subset of them.
-    Reply {
-        num: u16,
-        params: Vec<String>,
-    },
-}
-
-/// An intermediate type used during parsing.
-enum MsgType<'a> {
-    Cmd(&'a str),
-    Num(u16),
-}
-
-static CRLF: [u8; 2] = [b'\r', b'\n'];
-
-impl Msg {
-    /// Try to read an IRC message off a buffer. Drops the message when parsing is successful.
-    /// Otherwise the buffer is left unchanged.
-    pub fn read(buf: &mut Vec<u8>, logger: Option<LogFile>) -> Option<Msg> {
-        // find "\r\n" separator. `IntoSearcher` implementation for slice needs `str` (why??) so
-        // using this hacky method instead.
-        let crlf_idx = {
-            match buf.windows(2).position(|sub| sub == CRLF) {
-                None => return None,
-                Some(i) => i,
-            }
-        };
-
-        let ret = {
-            let mut slice: &[u8] = &buf[0..crlf_idx];
-
-            if let Some(mut logger) = logger {
-                match str::from_utf8(slice) {
-                    Ok(str) => {
-                        logger.write_line(format_args!("< {}", str));
-                    }
-                    Err(e) => {
-                        logger.write_line(format_args!("< (non-utf8: {:?}) {:?}", e, slice));
-                    }
-                }
-            }
-
-            let pfx: Option<Pfx> = {
-                if slice[0] == b':' {
-                    // parse prefix
-                    let ws_idx = find_byte(slice, b' ').unwrap();
-                    let (mut pfx, slice_) = slice.split_at(ws_idx);
-                    // drop the : from pfx
-                    pfx = &pfx[1..];
-                    slice = &slice_[1..]; // drop the space
-                    Some(parse_pfx(pfx))
-                } else {
-                    None
-                }
-            };
-
-            let msg_ty: MsgType = {
-                let ws_idx = find_byte(slice, b' ').unwrap();
-                let (cmd, slice_) = slice.split_at(ws_idx);
-                slice = &slice_[1..]; // drop the space
-                match parse_reply_num(cmd) {
-                    None => MsgType::Cmd(unsafe {
-                        // Cmd strings are added by the server and they're always ASCII strings, so
-                        // this is safe and O(1).
-                        str::from_utf8_unchecked(cmd)
-                    }),
-                    Some(num) => MsgType::Num(num),
-                }
-            };
-
-            let params: Vec<&str> = parse_params(unsafe { str::from_utf8_unchecked(slice) });
-            let cmd = match msg_ty {
-                MsgType::Cmd("PRIVMSG") | MsgType::Cmd("NOTICE") if params.len() == 2 => {
-                    let is_notice = if let MsgType::Cmd("NOTICE") = msg_ty {
-                        true
-                    } else {
-                        false
-                    };
-                    let target = params[0];
-                    let msg = params[1];
-                    let target = if target.chars().nth(0) == Some('#') {
-                        MsgTarget::Chan(target.to_owned())
-                    } else {
-                        MsgTarget::User(target.to_owned())
-                    };
-                    Cmd::PRIVMSG {
-                        target,
-                        msg: msg.to_owned(),
-                        is_notice,
-                    }
-                }
-                MsgType::Cmd("JOIN") if params.len() == 1 => {
-                    let chan = params[0];
-                    Cmd::JOIN {
-                        chan: chan.to_owned(),
-                    }
-                }
-                MsgType::Cmd("PART") if params.len() == 1 || params.len() == 2 => {
-                    let mb_msg = if params.len() == 2 {
-                        Some(params[1].to_owned())
-                    } else {
-                        None
-                    };
-                    Cmd::PART {
-                        chan: params[0].to_owned(),
-                        msg: mb_msg,
-                    }
-                }
-                MsgType::Cmd("QUIT") if params.len() == 0 || params.len() == 1 => {
-                    let mb_msg = if params.len() == 1 {
-                        Some(params[0].to_owned())
-                    } else {
-                        None
-                    };
-                    Cmd::QUIT { msg: mb_msg }
-                }
-                MsgType::Cmd("NICK") if params.len() == 1 => {
-                    let nick = params[0];
-                    Cmd::NICK {
-                        nick: nick.to_owned(),
-                    }
-                }
-                MsgType::Cmd("PING") if params.len() == 1 => Cmd::PING {
-                    server: params[0].to_owned(),
-                },
-                MsgType::Cmd("PONG") if params.len() >= 1 => Cmd::PONG {
-                    server: params[0].to_owned(),
-                },
-                MsgType::Cmd("ERROR") if params.len() == 1 => Cmd::ERROR {
-                    msg: params[0].to_owned(),
-                },
-                MsgType::Cmd("TOPIC") if params.len() == 2 => Cmd::TOPIC {
-                    chan: params[0].to_owned(),
-                    topic: params[1].to_owned(),
-                },
-                MsgType::Cmd("CAP") if params.len() == 3 => Cmd::CAP {
-                    client: params[0].to_owned(),
-                    subcommand: params[1].to_owned(),
-                    params: params[2].split(' ').map(|s| s.to_owned()).collect(),
-                },
-                MsgType::Cmd("AUTHENTICATE") if params.len() == 1 => Cmd::AUTHENTICATE {
-                    param: params[0].to_owned(),
-                },
-                MsgType::Num(n) => Cmd::Reply {
-                    num: n,
-                    params: params.into_iter().map(|s| s.to_owned()).collect(),
-                },
-                MsgType::Cmd(cmd) => Cmd::Other {
-                    cmd: cmd.to_owned(),
-                    params: params.into_iter().map(|s| s.to_owned()).collect(),
-                },
-            };
-
-            Msg { pfx, cmd }
-        };
-
-        buf.drain(0..crlf_idx + 2);
-        Some(ret)
-    }
-}
-
-fn parse_pfx(pfx: &[u8]) -> Pfx {
-    match find_byte(pfx, b'!') {
-        None => Pfx::Server(unsafe { str::from_utf8_unchecked(pfx).to_owned() }),
-        Some(idx) => Pfx::User {
-            nick: unsafe { str::from_utf8_unchecked(&pfx[0..idx]) }.to_owned(),
-            user: unsafe { str::from_utf8_unchecked(&pfx[idx + 1..]) }.to_owned(),
-        },
-    }
-}
-
-fn parse_reply_num(bs: &[u8]) -> Option<u16> {
-    fn is_num_ascii(b: u8) -> bool {
-        b >= b'0' && b <= b'9'
-    }
-
-    if bs.len() == 3 {
-        let n3 = bs[0];
-        let n2 = bs[1];
-        let n1 = bs[2];
-        if is_num_ascii(n3) && is_num_ascii(n2) && is_num_ascii(n1) {
-            return Some(
-                ((n3 - b'0') as u16) * 100 + ((n2 - b'0') as u16) * 10 + ((n1 - b'0') as u16),
-            );
-        }
-    }
-    None
-}
-
-fn parse_params(chrs: &str) -> Vec<&str> {
-    debug_assert_ne!(chrs.chars().nth(0), Some(' '));
-
-    let mut ret: Vec<&str> = Vec::new();
-
-    let mut slice_begins = 0;
-    for (char_idx, char) in chrs.char_indices() {
-        if char == ':' {
-            ret.push(unsafe { chrs.get_unchecked(char_idx + 1..chrs.len()) });
-            return ret;
-        } else if char == ' ' {
-            ret.push(unsafe { chrs.get_unchecked(slice_begins..char_idx) });
-            slice_begins = char_idx + 1;
-        }
-    }
-
-    if slice_begins != chrs.len() {
-        ret.push(unsafe { chrs.get_unchecked(slice_begins..chrs.len()) });
-    }
-
-    ret
-}
-
-pub fn find_byte(buf: &[u8], byte0: u8) -> Option<usize> {
-    for (byte_idx, byte) in buf.iter().enumerate() {
-        if *byte == byte0 {
-            return Some(byte_idx);
-        }
-    }
-    None
-}
-
-static CTCP_PREFIX: &'static str = "\x01ACTION ";
-
-pub fn check_ctcp_action_msg(msg: &str) -> (&str, bool) {
-    let msg_bytes = msg.as_bytes();
-    if msg_bytes.len() >= 8 && &msg_bytes[..8] == CTCP_PREFIX.as_bytes() {
-        (
-            if msg_bytes[msg.len() - 1] == 0x01 {
-                &msg[8..msg.len() - 1]
-            } else {
-                &msg[8..]
-            },
-            true,
-        )
-    } else {
-        (msg, false)
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use std::io::Write;
-
-    #[test]
-    fn test_parse_params() {
-        assert_eq!(parse_params("p1 p2 p3"), vec!["p1", "p2", "p3"]);
-        let v: Vec<&str> = vec![];
-        assert_eq!(parse_params(""), v);
-        assert_eq!(parse_params(":foo bar baz "), vec!["foo bar baz "]);
-        assert_eq!(parse_params(":"), vec![""]);
-    }
-
-    #[test]
-    fn test_privmsg_parsing() {
-        let mut buf = vec![];
-        write!(
-            &mut buf,
-            ":nick!~nick@unaffiliated/nick PRIVMSG tiny :a b c\r\n"
-        )
-        .unwrap();
-        assert_eq!(
-            Msg::read(&mut buf, None),
-            Some(Msg {
-                pfx: Some(Pfx::User {
-                    nick: "nick".to_owned(),
-                    user: "~nick@unaffiliated/nick".to_owned(),
-                }),
-                cmd: Cmd::PRIVMSG {
-                    target: MsgTarget::User("tiny".to_owned()),
-                    msg: "a b c".to_owned(),
-                    is_notice: false,
-                },
-            })
-        );
-        assert_eq!(buf.len(), 0);
-    }
-
-    #[test]
-    fn test_notice_parsing() {
-        let mut buf = vec![];
-        write!(
-            &mut buf,
-            ":barjavel.freenode.net NOTICE * :*** Looking up your hostname...\r\n"
-        )
-        .unwrap();
-        assert_eq!(
-            Msg::read(&mut buf, None),
-            Some(Msg {
-                pfx: Some(Pfx::Server("barjavel.freenode.net".to_owned())),
-                cmd: Cmd::PRIVMSG {
-                    target: MsgTarget::User("*".to_owned()),
-                    msg: "*** Looking up your hostname...".to_owned(),
-                    is_notice: true,
-                },
-            })
-        );
-    }
-
-    #[test]
-    fn test_numeric_parsing() {
-        let mut buf = vec![];
-        write!(
-            &mut buf,
-            ":barjavel.freenode.net 001 tiny :Welcome to the freenode Internet Relay Chat Network tiny\r\n"
-        ).unwrap();
-        write!(
-            &mut buf,
-            ":barjavel.freenode.net 002 tiny :Your host is barjavel.freenode.net[123.123.123.123/8001], \
-             running version ircd-seven-1.1.4\r\n"
-        ).unwrap();
-        write!(
-            &mut buf,
-            ":barjavel.freenode.net 004 tiny_test barjavel.freenode.net \
-             ircd-seven-1.1.4 DOQRSZaghilopswz \
-             CFILMPQSbcefgijklmnopqrstvz bkloveqjfI\r\n"
-        )
-        .unwrap();
-        write!(
-            &mut buf,
-            ":barjavel.freenode.net 005 tiny_test CHANTYPES=# EXCEPTS INVEX \
-             CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz CHANLIMIT=#:120 PREFIX=(ov)@+ \
-             MAXLIST=bqeI:100 MODES=4 NETWORK=freenode STATUSMSG=@+ CALLERID=g \
-             CASEMAPPING=rfc1459 :are supported by this server\r\n"
-        )
-        .unwrap();
-
-        let mut msgs = vec![];
-        while let Some(msg) = Msg::read(&mut buf, None) {
-            msgs.push(msg);
-        }
-
-        assert_eq!(msgs.len(), 4);
-    }
-
-    #[test]
-    fn test_part_parsing() {
-        let mut buf = vec![];
-        write!(&mut buf, ":tiny!~tiny@123.123.123.123 PART #haskell\r\n").unwrap();
-        assert_eq!(
-            Msg::read(&mut buf, None),
-            Some(Msg {
-                pfx: Some(Pfx::User {
-                    nick: "tiny".to_owned(),
-                    user: "~tiny@123.123.123.123".to_owned(),
-                }),
-                cmd: Cmd::PART {
-                    chan: "#haskell".to_owned(),
-                    msg: None,
-                },
-            })
-        );
-    }
-
-    #[test]
-    fn test_join_parsing() {
-        let mut buf = vec![];
-        write!(&mut buf, ":tiny!~tiny@192.168.0.1 JOIN #haskell\r\n").unwrap();
-        assert_eq!(
-            Msg::read(&mut buf, None),
-            Some(Msg {
-                pfx: Some(Pfx::User {
-                    nick: "tiny".to_owned(),
-                    user: "~tiny@192.168.0.1".to_owned(),
-                }),
-                cmd: Cmd::JOIN {
-                    chan: "#haskell".to_owned(),
-                },
-            })
-        );
-        assert_eq!(buf.len(), 0);
-    }
-
-    #[test]
-    fn test_ctcp_action_parsing() {
-        assert_eq!(
-            check_ctcp_action_msg("\x01ACTION msg contents\x01"),
-            ("msg contents", true)
-        );
-
-        // From https://modern.ircdocs.horse/ctcp.html:
-        //
-        // > The final <delim> MUST be sent, but parsers SHOULD accept incoming messages which lack
-        // > it (particularly for CTCP ACTION). This is due to how some software incorrectly
-        // > implements message splitting.
-        assert_eq!(
-            check_ctcp_action_msg("\x01ACTION msg contents"),
-            ("msg contents", true)
-        );
-
-        assert_eq!(check_ctcp_action_msg(""), ("", false));
-
-        assert_eq!(check_ctcp_action_msg("\x01ACTION "), ("", true));
-
-        assert_eq!(check_ctcp_action_msg("\x01ACTION"), ("\x01ACTION", false));
-    }
-
-    #[test]
-    fn test_error_parsing() {
-        let mut buf = vec![];
-        write!(
-            &mut buf,
-            "ERROR :Closing Link: 212.252.143.51 (Excess Flood)\r\n"
-        )
-        .unwrap();
-        assert_eq!(
-            Msg::read(&mut buf, None),
-            Some(Msg {
-                pfx: None,
-                cmd: Cmd::ERROR {
-                    msg: "Closing Link: 212.252.143.51 (Excess Flood)".to_owned(),
-                },
-            }),
-        );
-    }
-}
diff --git a/term_input/Cargo.toml b/term_input/Cargo.toml
index 6e9153a1..c0cc1d56 100644
--- a/term_input/Cargo.toml
+++ b/term_input/Cargo.toml
@@ -5,10 +5,15 @@ authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
 description = "Input handling for xterm-compatible terminals"
 repository = "https://github.com/osa1/tiny"
 license = "MIT"
+edition = "2018"
 
 [dependencies]
+futures-preview = "0.3.0-alpha.18"
 libc = "0.2"
+mio = "0.6"
 nix = "0.7"
+tokio = { git = "https://github.com/tokio-rs/tokio.git" }
+tokio-net = { git = "https://github.com/tokio-rs/tokio.git" }
 
 [dev-dependencies]
 mio = "0.6.9"
diff --git a/term_input/src/lib.rs b/term_input/src/lib.rs
index bd93aa58..81284350 100644
--- a/term_input/src/lib.rs
+++ b/term_input/src/lib.rs
@@ -1,3 +1,5 @@
+#![allow(clippy::new_without_default)]
+
 //! Interprets the terminal events we care about:
 //!
 //!   - Resize events.
@@ -8,15 +10,17 @@
 //! Keyboard events are read from stdin. We look for byte strings of key combinations that we care
 //! about. E.g. Alt-arrow keys, C-w etc.
 
-extern crate libc;
-extern crate nix;
-
 use nix::sys::signal;
 use nix::sys::signal::{sigaction, SigAction, SigHandler, SigSet, Signal};
 
 use std::char;
+use std::collections::VecDeque;
+use std::pin::Pin;
 use std::sync::atomic::AtomicBool;
 use std::sync::atomic::Ordering;
+use std::task::{Context, Poll};
+
+use tokio::prelude::*;
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 // Public types
@@ -68,11 +72,6 @@ pub enum Event {
     Unknown(Vec<u8>),
 }
 
-pub struct Input {
-    /// Used when reading from stdin.
-    buf: Vec<u8>,
-}
-
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 // SIGWINCH handler
 ////////////////////////////////////////////////////////////////////////////////////////////////////
@@ -118,7 +117,7 @@ static XTERM_END: [u8; 3] = [27, 91, 70];
 static XTERM_HOME_2: [u8; 3] = [27, 79, 72];
 static XTERM_END_2: [u8; 3] = [27, 79, 70];
 
-static XTERM_KEY_SEQS: [(&'static [u8], Event); 27] = [
+static XTERM_KEY_SEQS: [(&[u8], Event); 27] = [
     (
         &XTERM_ALT_ARROW_DOWN,
         Event::Key(Key::AltArrow(Arrow::Down)),
@@ -185,6 +184,16 @@ static XTERM_SINGLE_BYTES: [(u8, Event); 13] = [
 
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
+pub struct Input {
+    /// Queue of events waiting to be polled.
+    evs: VecDeque<Event>,
+
+    /// Used when reading from stdin.
+    buf: Vec<u8>,
+
+    stdin: tokio_net::util::PollEvented<mio::unix::EventedFd<'static>>,
+}
+
 impl Input {
     pub fn new() -> Input {
         unsafe {
@@ -201,24 +210,30 @@ impl Input {
         }
 
         Input {
+            evs: VecDeque::new(),
             buf: Vec::with_capacity(100),
+            stdin: tokio_net::util::PollEvented::new(mio::unix::EventedFd(&libc::STDIN_FILENO)),
         }
     }
+}
 
-    /// Fill the event buffer with pending events. Does not block.
-    pub fn read_input_events(&mut self, evs: &mut Vec<Event>) {
-        evs.clear();
+impl Stream for Input {
+    type Item = std::io::Result<Event>;
 
+    fn poll_next(
+        mut self: Pin<&mut Input>,
+        cx: &mut Context,
+    ) -> Poll<Option<std::io::Result<Event>>> {
+        // Yield any resize events
         if GOT_SIGWINCH.swap(false, Ordering::Relaxed) {
-            evs.push(Event::Resize);
+            return Poll::Ready(Some(Ok(Event::Resize)));
         }
 
-        if read_stdin(&mut self.buf) {
-            let mut buf_slice: &[u8] = &self.buf;
+        let self_: &mut Input = &mut *self;
 
-            // read_stdin() returned true, there should be at least one
-            // character in the buffer
-            debug_assert!(!buf_slice.is_empty());
+        // Try to parse any bytes in the input buffer from the last poll
+        {
+            let mut buf_slice: &[u8] = &self_.buf;
 
             while !buf_slice.is_empty() {
                 // Special treatment for 127 (backspace) and 13 ('\r')
@@ -229,21 +244,45 @@ impl Input {
                     read_chars
                 };
 
-                match read_fn(buf_slice, evs) {
+                match read_fn(buf_slice, &mut self_.evs) {
                     Some(buf_slice_) => {
                         buf_slice = buf_slice_;
                     }
                     None => {
-                        evs.push(Event::Unknown(buf_slice.to_owned()));
+                        self_.evs.push_back(Event::Unknown(buf_slice.to_owned()));
                         break;
                     }
                 }
             }
         }
+
+        self_.buf.clear();
+
+        // Yield pending events
+        if let Some(ev) = self_.evs.pop_front() {
+            return Poll::Ready(Some(Ok(ev)));
+        }
+
+        // Otherwise read stdin and loop if successful
+        match self_.stdin.poll_read_ready(cx, mio::Ready::readable()) {
+            Poll::Ready(Ok(_)) => {
+                if read_stdin(&mut self_.buf) {
+                    Input::poll_next(self, cx)
+                } else {
+                    self_
+                        .stdin
+                        .clear_read_ready(cx, mio::Ready::readable())
+                        .unwrap();
+                    Poll::Pending
+                }
+            }
+            Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
+            Poll::Pending => Poll::Pending,
+        }
     }
 }
 
-fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut Vec<Event>) -> Option<&'a [u8]> {
+fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
     debug_assert!(!buf_slice.is_empty());
 
     // Use a fast path for the common case: Single utf-8 character.
@@ -252,7 +291,7 @@ fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut Vec<Event>) -> Option<&'a [
     utf8_char_len(buf_slice[0]).map(|char_len| {
         if char_len as usize == buf_slice.len() {
             // fast path: single character
-            evs.push(Event::Key(Key::Char(get_utf8_char(buf_slice, char_len))));
+            evs.push_back(Event::Key(Key::Char(get_utf8_char(buf_slice, char_len))));
             &buf_slice[char_len as usize..]
         } else {
             // probably a paste: allocate a string and collect chars
@@ -271,26 +310,26 @@ fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut Vec<Event>) -> Option<&'a [
                     }
                 }
             }
-            evs.push(Event::String(string));
-            return buf_slice;
+            evs.push_back(Event::String(string));
+            buf_slice
         }
     })
 }
 
-fn read_key_comb<'a>(buf_slice: &'a [u8], evs: &mut Vec<Event>) -> Option<&'a [u8]> {
+fn read_key_comb<'a>(buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
     debug_assert!(!buf_slice.is_empty());
 
     // TODO: This is not working, see https://github.com/rust-lang/rust/issues/36401
     for &(byte, ref ev) in XTERM_SINGLE_BYTES.iter() {
         if byte == buf_slice[0] {
-            evs.push(ev.clone());
+            evs.push_back(ev.clone());
             return Some(&buf_slice[1..]);
         }
     }
 
     for &(byte_seq, ref ev) in XTERM_KEY_SEQS.iter() {
         if buf_slice.starts_with(byte_seq) {
-            evs.push(ev.clone());
+            evs.push_back(ev.clone());
             return Some(&buf_slice[byte_seq.len()..]);
         }
     }
@@ -298,14 +337,14 @@ fn read_key_comb<'a>(buf_slice: &'a [u8], evs: &mut Vec<Event>) -> Option<&'a [u
     if buf_slice[0] == 27 {
         // 27 not followed by anything is an actual ESC
         if buf_slice.len() == 1 {
-            evs.push(Event::Key(Key::Esc));
+            evs.push_back(Event::Key(Key::Esc));
             return Some(&buf_slice[1..]);
         }
         // otherwise it's probably alt + key
         else {
             debug_assert!(buf_slice.len() >= 2);
             return utf8_char_len(buf_slice[1]).map(|char_len| {
-                evs.push(Event::Key(Key::AltChar(get_utf8_char(
+                evs.push_back(Event::Key(Key::AltChar(get_utf8_char(
                     &buf_slice[1..],
                     char_len,
                 ))));
@@ -334,19 +373,19 @@ fn utf8_char_len(byte: u8) -> Option<u8> {
 fn get_utf8_char(buf: &[u8], len: u8) -> char {
     let codepoint: u32 = {
         if len == 1 {
-            (buf[0] & 0b0111_1111) as u32
+            u32::from(buf[0] & 0b0111_1111)
         } else if len == 2 {
-            (((buf[0] & 0b0001_1111) as u32) << 6) + ((buf[1] & 0b0011_1111) as u32)
+            ((u32::from(buf[0] & 0b0001_1111)) << 6) + (u32::from(buf[1] & 0b0011_1111))
         } else if len == 3 {
-            (((buf[0] & 0b0000_1111) as u32) << 12)
-                + (((buf[1] & 0b0011_1111) as u32) << 6)
-                + ((buf[2] & 0b0011_1111) as u32)
+            ((u32::from(buf[0] & 0b0000_1111)) << 12)
+                + ((u32::from(buf[1] & 0b0011_1111)) << 6)
+                + (u32::from(buf[2] & 0b0011_1111))
         } else {
             debug_assert!(len == 4);
-            (((buf[0] & 0b0000_0111) as u32) << 18)
-                + (((buf[1] & 0b0011_1111) as u32) << 12)
-                + (((buf[2] & 0b0011_1111) as u32) << 6)
-                + ((buf[3] & 0b0011_1111) as u32)
+            ((u32::from(buf[0] & 0b0000_0111)) << 18)
+                + ((u32::from(buf[1] & 0b0011_1111)) << 12)
+                + ((u32::from(buf[2] & 0b0011_1111)) << 6)
+                + (u32::from(buf[3] & 0b0011_1111))
         }
     };
 
diff --git a/term_input_futures/Cargo.toml b/term_input_futures/Cargo.toml
deleted file mode 100644
index afd8b637..00000000
--- a/term_input_futures/Cargo.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[package]
-name = "term_input_futures"
-version = "0.1.5"
-authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
-description = "Input handling for xterm-compatible terminals"
-repository = "https://github.com/osa1/tiny"
-license = "MIT"
-
-[dependencies]
-futures = "0.1"
-libc = "0.2"
-mio = "0.6"
-nix = "0.7"
-tokio = "0.1"
-
-[dev-dependencies]
-mio = "0.6.9"
diff --git a/term_input_futures/src/lib.rs b/term_input_futures/src/lib.rs
deleted file mode 100644
index af0a96be..00000000
--- a/term_input_futures/src/lib.rs
+++ /dev/null
@@ -1,410 +0,0 @@
-//! Interprets the terminal events we care about:
-//!
-//!   - Resize events.
-//!   - Keyboard input.
-//!
-//! Resize events are handled by registering a signal handler for SIGWINCH.
-//!
-//! Keyboard events are read from stdin. We look for byte strings of key combinations that we care
-//! about. E.g. Alt-arrow keys, C-w etc.
-
-extern crate futures;
-extern crate libc;
-extern crate mio;
-extern crate nix;
-extern crate tokio;
-
-use nix::sys::signal;
-use nix::sys::signal::{sigaction, SigAction, SigHandler, SigSet, Signal};
-
-use std::char;
-use std::collections::VecDeque;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering;
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Public types
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Key {
-    AltArrow(Arrow),
-    AltChar(char),
-    Arrow(Arrow),
-    Backspace,
-    Char(char),
-    Ctrl(char),
-    CtrlArrow(Arrow),
-    Del,
-    End,
-    Esc,
-    Home,
-    PageDown,
-    PageUp,
-    ShiftDown,
-    ShiftUp,
-    Tab,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Arrow {
-    Left,
-    Right,
-    Up,
-    Down,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
-    /// A single key input.
-    Key(Key),
-
-    /// Usually a paste.
-    String(String),
-
-    /// SIGWINCH happened.
-    Resize,
-
-    FocusGained,
-    FocusLost,
-
-    /// An unknown sequence of bytes (probably for a key combination that we don't care about).
-    Unknown(Vec<u8>),
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// SIGWINCH handler
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false);
-
-extern "C" fn sigwinch_handler(_: libc::c_int) {
-    GOT_SIGWINCH.store(true, Ordering::Relaxed);
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Byte sequences of key pressed we want to capture
-// (TODO: We only support xterm for now)
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static XTERM_ALT_ARROW_DOWN: [u8; 6] = [27, 91, 49, 59, 51, 66];
-static XTERM_ALT_ARROW_LEFT: [u8; 6] = [27, 91, 49, 59, 51, 68];
-static XTERM_ALT_ARROW_RIGHT: [u8; 6] = [27, 91, 49, 59, 51, 67];
-static XTERM_ALT_ARROW_UP: [u8; 6] = [27, 91, 49, 59, 51, 65];
-static XTERM_ARROW_DOWN: [u8; 3] = [27, 91, 66];
-static XTERM_ARROW_DOWN_2: [u8; 3] = [27, 79, 66];
-static XTERM_ARROW_LEFT: [u8; 3] = [27, 91, 68];
-static XTERM_ARROW_LEFT_2: [u8; 3] = [27, 79, 68];
-static XTERM_ARROW_RIGHT: [u8; 3] = [27, 91, 67];
-static XTERM_ARROW_RIGHT_2: [u8; 3] = [27, 79, 67];
-static XTERM_ARROW_UP: [u8; 3] = [27, 91, 65];
-static XTERM_ARROW_UP_2: [u8; 3] = [27, 79, 65];
-static XTERM_CTRL_ARROW_DOWN: [u8; 6] = [27, 91, 49, 59, 53, 66];
-static XTERM_CTRL_ARROW_LEFT: [u8; 6] = [27, 91, 49, 59, 53, 68];
-static XTERM_CTRL_ARROW_RIGHT: [u8; 6] = [27, 91, 49, 59, 53, 67];
-static XTERM_CTRL_ARROW_UP: [u8; 6] = [27, 91, 49, 59, 53, 65];
-static XTERM_DEL: [u8; 4] = [27, 91, 51, 126];
-static XTERM_PAGE_DOWN: [u8; 4] = [27, 91, 54, 126];
-static XTERM_PAGE_UP: [u8; 4] = [27, 91, 53, 126];
-static XTERM_SHIFT_UP: [u8; 6] = [27, 91, 49, 59, 50, 65];
-static XTERM_SHIFT_DOWN: [u8; 6] = [27, 91, 49, 59, 50, 66];
-static XTERM_FOCUS_GAINED: [u8; 3] = [27, 91, 73];
-static XTERM_FOCUS_LOST: [u8; 3] = [27, 91, 79];
-// FIXME: For some reason term_input test program gets first two of these bytes while tiny gets the
-// latter two. Tried to debug this a little bit by changing termattrs but no luck...
-static XTERM_HOME: [u8; 3] = [27, 91, 72];
-static XTERM_END: [u8; 3] = [27, 91, 70];
-static XTERM_HOME_2: [u8; 3] = [27, 79, 72];
-static XTERM_END_2: [u8; 3] = [27, 79, 70];
-
-static XTERM_KEY_SEQS: [(&'static [u8], Event); 27] = [
-    (
-        &XTERM_ALT_ARROW_DOWN,
-        Event::Key(Key::AltArrow(Arrow::Down)),
-    ),
-    (
-        &XTERM_ALT_ARROW_LEFT,
-        Event::Key(Key::AltArrow(Arrow::Left)),
-    ),
-    (
-        &XTERM_ALT_ARROW_RIGHT,
-        Event::Key(Key::AltArrow(Arrow::Right)),
-    ),
-    (&XTERM_ALT_ARROW_UP, Event::Key(Key::AltArrow(Arrow::Up))),
-    (&XTERM_ARROW_DOWN, Event::Key(Key::Arrow(Arrow::Down))),
-    (&XTERM_ARROW_DOWN_2, Event::Key(Key::Arrow(Arrow::Down))),
-    (&XTERM_ARROW_LEFT, Event::Key(Key::Arrow(Arrow::Left))),
-    (&XTERM_ARROW_LEFT_2, Event::Key(Key::Arrow(Arrow::Left))),
-    (&XTERM_ARROW_RIGHT, Event::Key(Key::Arrow(Arrow::Right))),
-    (&XTERM_ARROW_RIGHT_2, Event::Key(Key::Arrow(Arrow::Right))),
-    (&XTERM_ARROW_UP, Event::Key(Key::Arrow(Arrow::Up))),
-    (&XTERM_ARROW_UP_2, Event::Key(Key::Arrow(Arrow::Up))),
-    (
-        &XTERM_CTRL_ARROW_DOWN,
-        Event::Key(Key::CtrlArrow(Arrow::Down)),
-    ),
-    (
-        &XTERM_CTRL_ARROW_LEFT,
-        Event::Key(Key::CtrlArrow(Arrow::Left)),
-    ),
-    (
-        &XTERM_CTRL_ARROW_RIGHT,
-        Event::Key(Key::CtrlArrow(Arrow::Right)),
-    ),
-    (&XTERM_CTRL_ARROW_UP, Event::Key(Key::CtrlArrow(Arrow::Up))),
-    (&XTERM_DEL, Event::Key(Key::Del)),
-    (&XTERM_PAGE_DOWN, Event::Key(Key::PageDown)),
-    (&XTERM_PAGE_UP, Event::Key(Key::PageUp)),
-    (&XTERM_SHIFT_UP, Event::Key(Key::ShiftUp)),
-    (&XTERM_SHIFT_DOWN, Event::Key(Key::ShiftDown)),
-    (&XTERM_HOME, Event::Key(Key::Home)),
-    (&XTERM_END, Event::Key(Key::End)),
-    (&XTERM_HOME_2, Event::Key(Key::Home)),
-    (&XTERM_END_2, Event::Key(Key::End)),
-    (&XTERM_FOCUS_GAINED, Event::FocusGained),
-    (&XTERM_FOCUS_LOST, Event::FocusLost),
-];
-
-// Make sure not to use 27 (ESC) because it's used as a prefix in many combinations.
-static XTERM_SINGLE_BYTES: [(u8, Event); 13] = [
-    (9, Event::Key(Key::Tab)),
-    (127, Event::Key(Key::Backspace)),
-    (1, Event::Key(Key::Ctrl('a'))),
-    (5, Event::Key(Key::Ctrl('e'))),
-    (23, Event::Key(Key::Ctrl('w'))),
-    (11, Event::Key(Key::Ctrl('k'))),
-    (4, Event::Key(Key::Ctrl('d'))),
-    (3, Event::Key(Key::Ctrl('c'))),
-    (17, Event::Key(Key::Ctrl('q'))),
-    (16, Event::Key(Key::Ctrl('p'))),
-    (14, Event::Key(Key::Ctrl('n'))),
-    (21, Event::Key(Key::Ctrl('u'))),
-    (24, Event::Key(Key::Ctrl('x'))),
-];
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-pub struct Input {
-    /// Queue of events waiting to be polled.
-    evs: VecDeque<Event>,
-
-    /// Used when reading from stdin.
-    buf: Vec<u8>,
-
-    stdin: tokio::reactor::PollEvented2<mio::unix::EventedFd<'static>>,
-}
-
-impl Input {
-    pub fn new() -> Input {
-        unsafe {
-            // ignore the existing handler
-            sigaction(
-                Signal::SIGWINCH,
-                &SigAction::new(
-                    SigHandler::Handler(sigwinch_handler),
-                    signal::SA_RESTART,
-                    SigSet::empty(),
-                ),
-            )
-            .unwrap();
-        }
-
-        Input {
-            evs: VecDeque::new(),
-            buf: Vec::with_capacity(100),
-            stdin: tokio::reactor::PollEvented2::new(mio::unix::EventedFd(&libc::STDIN_FILENO)),
-        }
-    }
-}
-
-impl futures::Stream for Input {
-    type Item = Event;
-    type Error = tokio::io::Error;
-
-    fn poll(&mut self) -> Result<futures::Async<Option<Event>>, tokio::io::Error> {
-        // Yield any resize events
-        if GOT_SIGWINCH.swap(false, Ordering::Relaxed) {
-            return Ok(futures::Async::Ready(Some(Event::Resize)));
-        }
-
-        // Try to parse any bytes in the input buffer from the last poll
-        {
-            let mut buf_slice: &[u8] = &mut self.buf;
-            while !buf_slice.is_empty() {
-                // Special treatment for 127 (backspace) and 13 ('\r')
-                let fst = buf_slice[0];
-                let read_fn = if (fst < 32 && fst != 13) || fst == 127 {
-                    read_key_comb
-                } else {
-                    read_chars
-                };
-
-                match read_fn(buf_slice, &mut self.evs) {
-                    Some(buf_slice_) => {
-                        buf_slice = buf_slice_;
-                    }
-                    None => {
-                        self.evs.push_back(Event::Unknown(buf_slice.to_owned()));
-                        break;
-                    }
-                }
-            }
-        }
-        self.buf.clear();
-
-        // Yield pending events
-        if let Some(ev) = self.evs.pop_front() {
-            return Ok(futures::Async::Ready(Some(ev)));
-        }
-
-        // Otherwise read stdin and loop if successful
-        match self.stdin.poll_read_ready(mio::Ready::readable()) {
-            Ok(futures::Async::Ready(_)) => {
-                if read_stdin(&mut self.buf) {
-                    self.poll()
-                } else {
-                    self.stdin.clear_read_ready(mio::Ready::readable()).unwrap();
-                    Ok(futures::Async::NotReady)
-                }
-            }
-            Ok(futures::Async::NotReady) => Ok(futures::Async::NotReady),
-            Err(err) => Err(err),
-        }
-    }
-}
-
-fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
-    debug_assert!(!buf_slice.is_empty());
-
-    // Use a fast path for the common case: Single utf-8 character.
-    // (TODO: What about other encodings?)
-
-    utf8_char_len(buf_slice[0]).map(|char_len| {
-        if char_len as usize == buf_slice.len() {
-            // fast path: single character
-            evs.push_back(Event::Key(Key::Char(get_utf8_char(buf_slice, char_len))));
-            &buf_slice[char_len as usize..]
-        } else {
-            // probably a paste: allocate a string and collect chars
-            let mut string = String::with_capacity(10);
-            loop {
-                if buf_slice.is_empty() {
-                    break;
-                }
-                match utf8_char_len(buf_slice[0]) {
-                    Some(char_len) => {
-                        string.push(get_utf8_char(buf_slice, char_len));
-                        buf_slice = &buf_slice[char_len as usize..];
-                    }
-                    None => {
-                        break;
-                    }
-                }
-            }
-            evs.push_back(Event::String(string));
-            return buf_slice;
-        }
-    })
-}
-
-fn read_key_comb<'a>(buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
-    debug_assert!(!buf_slice.is_empty());
-
-    // TODO: This is not working, see https://github.com/rust-lang/rust/issues/36401
-    for &(byte, ref ev) in XTERM_SINGLE_BYTES.iter() {
-        if byte == buf_slice[0] {
-            evs.push_back(ev.clone());
-            return Some(&buf_slice[1..]);
-        }
-    }
-
-    for &(byte_seq, ref ev) in XTERM_KEY_SEQS.iter() {
-        if buf_slice.starts_with(byte_seq) {
-            evs.push_back(ev.clone());
-            return Some(&buf_slice[byte_seq.len()..]);
-        }
-    }
-
-    if buf_slice[0] == 27 {
-        // 27 not followed by anything is an actual ESC
-        if buf_slice.len() == 1 {
-            evs.push_back(Event::Key(Key::Esc));
-            return Some(&buf_slice[1..]);
-        }
-        // otherwise it's probably alt + key
-        else {
-            debug_assert!(buf_slice.len() >= 2);
-            return utf8_char_len(buf_slice[1]).map(|char_len| {
-                evs.push_back(Event::Key(Key::AltChar(get_utf8_char(
-                    &buf_slice[1..],
-                    char_len,
-                ))));
-                &buf_slice[char_len as usize + 1..]
-            });
-        }
-    }
-
-    None
-}
-
-fn utf8_char_len(byte: u8) -> Option<u8> {
-    if byte >> 7 == 0b0 {
-        Some(1)
-    } else if byte >> 5 == 0b110 {
-        Some(2)
-    } else if byte >> 4 == 0b1110 {
-        Some(3)
-    } else if byte >> 3 == 0b11110 {
-        Some(4)
-    } else {
-        None
-    }
-}
-
-fn get_utf8_char(buf: &[u8], len: u8) -> char {
-    let codepoint: u32 = {
-        if len == 1 {
-            (buf[0] & 0b0111_1111) as u32
-        } else if len == 2 {
-            (((buf[0] & 0b0001_1111) as u32) << 6) + ((buf[1] & 0b0011_1111) as u32)
-        } else if len == 3 {
-            (((buf[0] & 0b0000_1111) as u32) << 12)
-                + (((buf[1] & 0b0011_1111) as u32) << 6)
-                + ((buf[2] & 0b0011_1111) as u32)
-        } else {
-            debug_assert!(len == 4);
-            (((buf[0] & 0b0000_0111) as u32) << 18)
-                + (((buf[1] & 0b0011_1111) as u32) << 12)
-                + (((buf[2] & 0b0011_1111) as u32) << 6)
-                + ((buf[3] & 0b0011_1111) as u32)
-        }
-    };
-
-    char::from_u32(codepoint).unwrap()
-}
-
-/// Read stdin contents if it's ready for reading. Returns true when it was able to read. Buffer is
-/// not modified when return value is 0.
-pub fn read_stdin(buf: &mut Vec<u8>) -> bool {
-    let mut bytes_available: i32 = 0; // this really needs to be a 32-bit value
-    let ioctl_ret =
-        unsafe { libc::ioctl(libc::STDIN_FILENO, libc::FIONREAD, &mut bytes_available) };
-    // println!("ioctl_ret: {}", ioctl_ret);
-    // println!("bytes_available: {}", bytes_available);
-    if ioctl_ret < 0 || bytes_available == 0 {
-        false
-    } else {
-        buf.clear();
-        buf.reserve(bytes_available as usize);
-
-        let buf_ptr: *mut libc::c_void = buf.as_ptr() as *mut libc::c_void;
-        let bytes_read =
-            unsafe { libc::read(libc::STDIN_FILENO, buf_ptr, bytes_available as usize) };
-        debug_assert!(bytes_read == bytes_available as isize);
-
-        unsafe {
-            buf.set_len(bytes_read as usize);
-        }
-        true
-    }
-}
diff --git a/term_input_std_futures/Cargo.toml b/term_input_std_futures/Cargo.toml
deleted file mode 100644
index 8cb5ffe2..00000000
--- a/term_input_std_futures/Cargo.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[package]
-name = "term_input_std_futures"
-version = "0.1.5"
-authors = ["Ömer Sinan Ağacan <omeragacan@gmail.com>"]
-description = "Input handling for xterm-compatible terminals"
-repository = "https://github.com/osa1/tiny"
-license = "MIT"
-edition = "2018"
-
-[dependencies]
-futures-preview = "0.3.0-alpha.18"
-libc = "0.2"
-mio = "0.6"
-nix = "0.7"
-tokio = "0.2.0-alpha.2"
-tokio-net = "0.2.0-alpha.2"
-
-[dev-dependencies]
-mio = "0.6.9"
diff --git a/term_input_std_futures/src/lib.rs b/term_input_std_futures/src/lib.rs
deleted file mode 100644
index 11947afa..00000000
--- a/term_input_std_futures/src/lib.rs
+++ /dev/null
@@ -1,423 +0,0 @@
-//! Interprets the terminal events we care about:
-//!
-//!   - Resize events.
-//!   - Keyboard input.
-//!
-//! Resize events are handled by registering a signal handler for SIGWINCH.
-//!
-//! Keyboard events are read from stdin. We look for byte strings of key combinations that we care
-//! about. E.g. Alt-arrow keys, C-w etc.
-
-extern crate libc;
-extern crate mio;
-extern crate nix;
-extern crate tokio;
-extern crate tokio_net;
-
-use nix::sys::signal;
-use nix::sys::signal::{sigaction, SigAction, SigHandler, SigSet, Signal};
-
-use std::char;
-use std::collections::VecDeque;
-use std::pin::Pin;
-use std::sync::atomic::AtomicBool;
-use std::sync::atomic::Ordering;
-use std::task::{Context, Poll};
-
-use tokio::prelude::*;
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Public types
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Key {
-    AltArrow(Arrow),
-    AltChar(char),
-    Arrow(Arrow),
-    Backspace,
-    Char(char),
-    Ctrl(char),
-    CtrlArrow(Arrow),
-    Del,
-    End,
-    Esc,
-    Home,
-    PageDown,
-    PageUp,
-    ShiftDown,
-    ShiftUp,
-    Tab,
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum Arrow {
-    Left,
-    Right,
-    Up,
-    Down,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Event {
-    /// A single key input.
-    Key(Key),
-
-    /// Usually a paste.
-    String(String),
-
-    /// SIGWINCH happened.
-    Resize,
-
-    FocusGained,
-    FocusLost,
-
-    /// An unknown sequence of bytes (probably for a key combination that we don't care about).
-    Unknown(Vec<u8>),
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// SIGWINCH handler
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static GOT_SIGWINCH: AtomicBool = AtomicBool::new(false);
-
-extern "C" fn sigwinch_handler(_: libc::c_int) {
-    GOT_SIGWINCH.store(true, Ordering::Relaxed);
-}
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-// Byte sequences of key pressed we want to capture
-// (TODO: We only support xterm for now)
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-static XTERM_ALT_ARROW_DOWN: [u8; 6] = [27, 91, 49, 59, 51, 66];
-static XTERM_ALT_ARROW_LEFT: [u8; 6] = [27, 91, 49, 59, 51, 68];
-static XTERM_ALT_ARROW_RIGHT: [u8; 6] = [27, 91, 49, 59, 51, 67];
-static XTERM_ALT_ARROW_UP: [u8; 6] = [27, 91, 49, 59, 51, 65];
-static XTERM_ARROW_DOWN: [u8; 3] = [27, 91, 66];
-static XTERM_ARROW_DOWN_2: [u8; 3] = [27, 79, 66];
-static XTERM_ARROW_LEFT: [u8; 3] = [27, 91, 68];
-static XTERM_ARROW_LEFT_2: [u8; 3] = [27, 79, 68];
-static XTERM_ARROW_RIGHT: [u8; 3] = [27, 91, 67];
-static XTERM_ARROW_RIGHT_2: [u8; 3] = [27, 79, 67];
-static XTERM_ARROW_UP: [u8; 3] = [27, 91, 65];
-static XTERM_ARROW_UP_2: [u8; 3] = [27, 79, 65];
-static XTERM_CTRL_ARROW_DOWN: [u8; 6] = [27, 91, 49, 59, 53, 66];
-static XTERM_CTRL_ARROW_LEFT: [u8; 6] = [27, 91, 49, 59, 53, 68];
-static XTERM_CTRL_ARROW_RIGHT: [u8; 6] = [27, 91, 49, 59, 53, 67];
-static XTERM_CTRL_ARROW_UP: [u8; 6] = [27, 91, 49, 59, 53, 65];
-static XTERM_DEL: [u8; 4] = [27, 91, 51, 126];
-static XTERM_PAGE_DOWN: [u8; 4] = [27, 91, 54, 126];
-static XTERM_PAGE_UP: [u8; 4] = [27, 91, 53, 126];
-static XTERM_SHIFT_UP: [u8; 6] = [27, 91, 49, 59, 50, 65];
-static XTERM_SHIFT_DOWN: [u8; 6] = [27, 91, 49, 59, 50, 66];
-static XTERM_FOCUS_GAINED: [u8; 3] = [27, 91, 73];
-static XTERM_FOCUS_LOST: [u8; 3] = [27, 91, 79];
-// FIXME: For some reason term_input test program gets first two of these bytes while tiny gets the
-// latter two. Tried to debug this a little bit by changing termattrs but no luck...
-static XTERM_HOME: [u8; 3] = [27, 91, 72];
-static XTERM_END: [u8; 3] = [27, 91, 70];
-static XTERM_HOME_2: [u8; 3] = [27, 79, 72];
-static XTERM_END_2: [u8; 3] = [27, 79, 70];
-
-static XTERM_KEY_SEQS: [(&'static [u8], Event); 27] = [
-    (
-        &XTERM_ALT_ARROW_DOWN,
-        Event::Key(Key::AltArrow(Arrow::Down)),
-    ),
-    (
-        &XTERM_ALT_ARROW_LEFT,
-        Event::Key(Key::AltArrow(Arrow::Left)),
-    ),
-    (
-        &XTERM_ALT_ARROW_RIGHT,
-        Event::Key(Key::AltArrow(Arrow::Right)),
-    ),
-    (&XTERM_ALT_ARROW_UP, Event::Key(Key::AltArrow(Arrow::Up))),
-    (&XTERM_ARROW_DOWN, Event::Key(Key::Arrow(Arrow::Down))),
-    (&XTERM_ARROW_DOWN_2, Event::Key(Key::Arrow(Arrow::Down))),
-    (&XTERM_ARROW_LEFT, Event::Key(Key::Arrow(Arrow::Left))),
-    (&XTERM_ARROW_LEFT_2, Event::Key(Key::Arrow(Arrow::Left))),
-    (&XTERM_ARROW_RIGHT, Event::Key(Key::Arrow(Arrow::Right))),
-    (&XTERM_ARROW_RIGHT_2, Event::Key(Key::Arrow(Arrow::Right))),
-    (&XTERM_ARROW_UP, Event::Key(Key::Arrow(Arrow::Up))),
-    (&XTERM_ARROW_UP_2, Event::Key(Key::Arrow(Arrow::Up))),
-    (
-        &XTERM_CTRL_ARROW_DOWN,
-        Event::Key(Key::CtrlArrow(Arrow::Down)),
-    ),
-    (
-        &XTERM_CTRL_ARROW_LEFT,
-        Event::Key(Key::CtrlArrow(Arrow::Left)),
-    ),
-    (
-        &XTERM_CTRL_ARROW_RIGHT,
-        Event::Key(Key::CtrlArrow(Arrow::Right)),
-    ),
-    (&XTERM_CTRL_ARROW_UP, Event::Key(Key::CtrlArrow(Arrow::Up))),
-    (&XTERM_DEL, Event::Key(Key::Del)),
-    (&XTERM_PAGE_DOWN, Event::Key(Key::PageDown)),
-    (&XTERM_PAGE_UP, Event::Key(Key::PageUp)),
-    (&XTERM_SHIFT_UP, Event::Key(Key::ShiftUp)),
-    (&XTERM_SHIFT_DOWN, Event::Key(Key::ShiftDown)),
-    (&XTERM_HOME, Event::Key(Key::Home)),
-    (&XTERM_END, Event::Key(Key::End)),
-    (&XTERM_HOME_2, Event::Key(Key::Home)),
-    (&XTERM_END_2, Event::Key(Key::End)),
-    (&XTERM_FOCUS_GAINED, Event::FocusGained),
-    (&XTERM_FOCUS_LOST, Event::FocusLost),
-];
-
-// Make sure not to use 27 (ESC) because it's used as a prefix in many combinations.
-static XTERM_SINGLE_BYTES: [(u8, Event); 13] = [
-    (9, Event::Key(Key::Tab)),
-    (127, Event::Key(Key::Backspace)),
-    (1, Event::Key(Key::Ctrl('a'))),
-    (5, Event::Key(Key::Ctrl('e'))),
-    (23, Event::Key(Key::Ctrl('w'))),
-    (11, Event::Key(Key::Ctrl('k'))),
-    (4, Event::Key(Key::Ctrl('d'))),
-    (3, Event::Key(Key::Ctrl('c'))),
-    (17, Event::Key(Key::Ctrl('q'))),
-    (16, Event::Key(Key::Ctrl('p'))),
-    (14, Event::Key(Key::Ctrl('n'))),
-    (21, Event::Key(Key::Ctrl('u'))),
-    (24, Event::Key(Key::Ctrl('x'))),
-];
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-
-pub struct Input {
-    /// Queue of events waiting to be polled.
-    evs: VecDeque<Event>,
-
-    /// Used when reading from stdin.
-    buf: Vec<u8>,
-
-    stdin: tokio_net::util::PollEvented<mio::unix::EventedFd<'static>>,
-}
-
-impl Input {
-    pub fn new() -> Input {
-        unsafe {
-            // ignore the existing handler
-            sigaction(
-                Signal::SIGWINCH,
-                &SigAction::new(
-                    SigHandler::Handler(sigwinch_handler),
-                    signal::SA_RESTART,
-                    SigSet::empty(),
-                ),
-            )
-            .unwrap();
-        }
-
-        Input {
-            evs: VecDeque::new(),
-            buf: Vec::with_capacity(100),
-            stdin: tokio_net::util::PollEvented::new(mio::unix::EventedFd(&libc::STDIN_FILENO)),
-        }
-    }
-}
-
-impl Stream for Input {
-    type Item = std::io::Result<Event>;
-
-    fn poll_next(
-        mut self: Pin<&mut Input>,
-        cx: &mut Context,
-    ) -> Poll<Option<std::io::Result<Event>>> {
-        // Yield any resize events
-        if GOT_SIGWINCH.swap(false, Ordering::Relaxed) {
-            return Poll::Ready(Some(Ok(Event::Resize)));
-        }
-
-        let self_: &mut Input = &mut *self;
-
-        // Try to parse any bytes in the input buffer from the last poll
-        {
-            let mut buf_slice: &[u8] = &self_.buf;
-
-            while !buf_slice.is_empty() {
-                // Special treatment for 127 (backspace) and 13 ('\r')
-                let fst = buf_slice[0];
-                let read_fn = if (fst < 32 && fst != 13) || fst == 127 {
-                    read_key_comb
-                } else {
-                    read_chars
-                };
-
-                match read_fn(buf_slice, &mut self_.evs) {
-                    Some(buf_slice_) => {
-                        buf_slice = buf_slice_;
-                    }
-                    None => {
-                        self_.evs.push_back(Event::Unknown(buf_slice.to_owned()));
-                        break;
-                    }
-                }
-            }
-        }
-
-        self_.buf.clear();
-
-        // Yield pending events
-        if let Some(ev) = self_.evs.pop_front() {
-            return Poll::Ready(Some(Ok(ev)));
-        }
-
-        // Otherwise read stdin and loop if successful
-        match self_.stdin.poll_read_ready(cx, mio::Ready::readable()) {
-            Poll::Ready(Ok(_)) => {
-                if read_stdin(&mut self_.buf) {
-                    Input::poll_next(self, cx)
-                } else {
-                    self_
-                        .stdin
-                        .clear_read_ready(cx, mio::Ready::readable())
-                        .unwrap();
-                    Poll::Pending
-                }
-            }
-            Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
-            Poll::Pending => Poll::Pending,
-        }
-    }
-}
-
-fn read_chars<'a>(mut buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
-    debug_assert!(!buf_slice.is_empty());
-
-    // Use a fast path for the common case: Single utf-8 character.
-    // (TODO: What about other encodings?)
-
-    utf8_char_len(buf_slice[0]).map(|char_len| {
-        if char_len as usize == buf_slice.len() {
-            // fast path: single character
-            evs.push_back(Event::Key(Key::Char(get_utf8_char(buf_slice, char_len))));
-            &buf_slice[char_len as usize..]
-        } else {
-            // probably a paste: allocate a string and collect chars
-            let mut string = String::with_capacity(10);
-            loop {
-                if buf_slice.is_empty() {
-                    break;
-                }
-                match utf8_char_len(buf_slice[0]) {
-                    Some(char_len) => {
-                        string.push(get_utf8_char(buf_slice, char_len));
-                        buf_slice = &buf_slice[char_len as usize..];
-                    }
-                    None => {
-                        break;
-                    }
-                }
-            }
-            evs.push_back(Event::String(string));
-            return buf_slice;
-        }
-    })
-}
-
-fn read_key_comb<'a>(buf_slice: &'a [u8], evs: &mut VecDeque<Event>) -> Option<&'a [u8]> {
-    debug_assert!(!buf_slice.is_empty());
-
-    // TODO: This is not working, see https://github.com/rust-lang/rust/issues/36401
-    for &(byte, ref ev) in XTERM_SINGLE_BYTES.iter() {
-        if byte == buf_slice[0] {
-            evs.push_back(ev.clone());
-            return Some(&buf_slice[1..]);
-        }
-    }
-
-    for &(byte_seq, ref ev) in XTERM_KEY_SEQS.iter() {
-        if buf_slice.starts_with(byte_seq) {
-            evs.push_back(ev.clone());
-            return Some(&buf_slice[byte_seq.len()..]);
-        }
-    }
-
-    if buf_slice[0] == 27 {
-        // 27 not followed by anything is an actual ESC
-        if buf_slice.len() == 1 {
-            evs.push_back(Event::Key(Key::Esc));
-            return Some(&buf_slice[1..]);
-        }
-        // otherwise it's probably alt + key
-        else {
-            debug_assert!(buf_slice.len() >= 2);
-            return utf8_char_len(buf_slice[1]).map(|char_len| {
-                evs.push_back(Event::Key(Key::AltChar(get_utf8_char(
-                    &buf_slice[1..],
-                    char_len,
-                ))));
-                &buf_slice[char_len as usize + 1..]
-            });
-        }
-    }
-
-    None
-}
-
-fn utf8_char_len(byte: u8) -> Option<u8> {
-    if byte >> 7 == 0b0 {
-        Some(1)
-    } else if byte >> 5 == 0b110 {
-        Some(2)
-    } else if byte >> 4 == 0b1110 {
-        Some(3)
-    } else if byte >> 3 == 0b11110 {
-        Some(4)
-    } else {
-        None
-    }
-}
-
-fn get_utf8_char(buf: &[u8], len: u8) -> char {
-    let codepoint: u32 = {
-        if len == 1 {
-            (buf[0] & 0b0111_1111) as u32
-        } else if len == 2 {
-            (((buf[0] & 0b0001_1111) as u32) << 6) + ((buf[1] & 0b0011_1111) as u32)
-        } else if len == 3 {
-            (((buf[0] & 0b0000_1111) as u32) << 12)
-                + (((buf[1] & 0b0011_1111) as u32) << 6)
-                + ((buf[2] & 0b0011_1111) as u32)
-        } else {
-            debug_assert!(len == 4);
-            (((buf[0] & 0b0000_0111) as u32) << 18)
-                + (((buf[1] & 0b0011_1111) as u32) << 12)
-                + (((buf[2] & 0b0011_1111) as u32) << 6)
-                + ((buf[3] & 0b0011_1111) as u32)
-        }
-    };
-
-    char::from_u32(codepoint).unwrap()
-}
-
-/// Read stdin contents if it's ready for reading. Returns true when it was able to read. Buffer is
-/// not modified when return value is 0.
-pub fn read_stdin(buf: &mut Vec<u8>) -> bool {
-    let mut bytes_available: i32 = 0; // this really needs to be a 32-bit value
-    let ioctl_ret =
-        unsafe { libc::ioctl(libc::STDIN_FILENO, libc::FIONREAD, &mut bytes_available) };
-    // println!("ioctl_ret: {}", ioctl_ret);
-    // println!("bytes_available: {}", bytes_available);
-    if ioctl_ret < 0 || bytes_available == 0 {
-        false
-    } else {
-        buf.clear();
-        buf.reserve(bytes_available as usize);
-
-        let buf_ptr: *mut libc::c_void = buf.as_ptr() as *mut libc::c_void;
-        let bytes_read =
-            unsafe { libc::read(libc::STDIN_FILENO, buf_ptr, bytes_available as usize) };
-        debug_assert!(bytes_read == bytes_available as isize);
-
-        unsafe {
-            buf.set_len(bytes_read as usize);
-        }
-        true
-    }
-}
diff --git a/termbox/Cargo.toml b/termbox/Cargo.toml
index abe936f2..3a436bab 100644
--- a/termbox/Cargo.toml
+++ b/termbox/Cargo.toml
@@ -7,6 +7,7 @@ description = "A hacked version of termbox (see README)"
 repository = "https://github.com/osa1/tiny"
 license = "MIT"
 build = "build.rs"
+edition = "2018"
 
 [dependencies]
 libc = "0.2"
@@ -16,4 +17,4 @@ cc = "1.0.38"
 
 [dev-dependencies]
 mio = "0.6.9"
-term_input = { path = "../term_input", version = "0.1.3" }
+term_input = { path = "../term_input" }
diff --git a/termbox/build.rs b/termbox/build.rs
index 32147eeb..6e4970fe 100644
--- a/termbox/build.rs
+++ b/termbox/build.rs
@@ -1,5 +1,3 @@
-extern crate cc;
-
 fn main() {
     cc::Build::new()
         .file("cbits/termbox.c")
diff --git a/termbox/examples/input.rs b/termbox/examples/input.rs
index 81af99e5..eea4b98d 100644
--- a/termbox/examples/input.rs
+++ b/termbox/examples/input.rs
@@ -1,8 +1,3 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-extern crate termbox_simple;
-
 use mio::unix::EventedFd;
 use mio::Events;
 use mio::Poll;
diff --git a/termbox/src/lib.rs b/termbox/src/lib.rs
index 28402540..fe5bcdb3 100644
--- a/termbox/src/lib.rs
+++ b/termbox/src/lib.rs
@@ -1,5 +1,3 @@
-extern crate libc;
-
 pub const TB_DEFAULT: u16 = 0x00;
 pub const TB_BLACK: u16 = 0x01;
 pub const TB_RED: u16 = 0x02;
@@ -162,16 +160,16 @@ fn char_to_utf8(c: char) -> u32 {
     if code < MAX_ONE_B {
         code as u32
     } else if code < MAX_TWO_B {
-        ((((code >> 6 & 0x1F) as u8 | TAG_TWO_B) as u32) << 8)
-            + (((code & 0x3F) as u8 | TAG_CONT) as u32)
+        ((u32::from((code >> 6 & 0x1F) as u8 | TAG_TWO_B)) << 8)
+            + u32::from((code & 0x3F) as u8 | TAG_CONT)
     } else if code < MAX_THREE_B {
-        ((((code >> 12 & 0x0F) as u8 | TAG_THREE_B) as u32) << 16)
-            + ((((code >> 6 & 0x3F) as u8 | TAG_CONT) as u32) << 8)
-            + (((code & 0x3F) as u8 | TAG_CONT) as u32)
+        (u32::from((code >> 12 & 0x0F) as u8 | TAG_THREE_B) << 16)
+            + (u32::from((code >> 6 & 0x3F) as u8 | TAG_CONT) << 8)
+            + (u32::from((code & 0x3F) as u8 | TAG_CONT))
     } else {
-        ((((code >> 18 & 0x07) as u8 | TAG_FOUR_B) as u32) << 24)
-            + ((((code >> 12 & 0x3F) as u8 | TAG_CONT) as u32) << 16)
-            + ((((code >> 6 & 0x3F) as u8 | TAG_CONT) as u32) << 8)
-            + (((code & 0x3F) as u8 | TAG_CONT) as u32)
+        ((u32::from((code >> 18 & 0x07) as u8 | TAG_FOUR_B)) << 24)
+            + ((u32::from((code >> 12 & 0x3F) as u8 | TAG_CONT)) << 16)
+            + ((u32::from((code >> 6 & 0x3F) as u8 | TAG_CONT)) << 8)
+            + (u32::from((code & 0x3F) as u8 | TAG_CONT))
     }
 }


Test patch:
diff --git a/test/lipsum.txt b/libtiny_tui/test/lipsum.txt
similarity index 100%
rename from test/lipsum.txt
rename to libtiny_tui/test/lipsum.txt
diff --git a/term_input/examples/test.rs b/term_input/examples/test.rs
index 20094977..68308687 100644
--- a/term_input/examples/test.rs
+++ b/term_input/examples/test.rs
@@ -1,14 +1,6 @@
-extern crate libc;
-extern crate mio;
-extern crate term_input;
-
-use mio::unix::EventedFd;
-use mio::Events;
-use mio::Poll;
-use mio::PollOpt;
-use mio::Ready;
-use mio::Token;
+#![feature(async_closure)]
 
+use futures::stream::StreamExt;
 use std::io;
 use std::io::Write;
 
@@ -40,34 +32,26 @@ fn main() {
         stdout.lock().flush().unwrap();
     }
 
-    let poll = Poll::new().unwrap();
-    poll.register(
-        &EventedFd(&libc::STDIN_FILENO),
-        Token(libc::STDIN_FILENO as usize),
-        Ready::readable(),
-        PollOpt::level(),
-    )
-    .unwrap();
-
+    /* DO THE BUSINESS HERE */
     let mut input = Input::new();
-    let mut evs = Vec::new();
-    let mut events = Events::with_capacity(10);
-    'mainloop: loop {
-        let _poll_ret = poll.poll(&mut events, None);
-        // println!("poll ret: {:?}", _poll_ret);
-
-        // Err: probably SIGWINCH
-        // Ok: stdin available
-        //
-        // there are events to handle either way
-        input.read_input_events(&mut evs);
-        println!("{:?}", evs);
-        for ev in evs.iter() {
-            if ev == &Event::Key(Key::Esc) {
-                break 'mainloop;
+    let mut executor = tokio::runtime::current_thread::Runtime::new().unwrap();
+    executor.spawn(async move {
+        while let Some(mb_ev) = input.next().await {
+            match mb_ev {
+                Ok(ev) => {
+                    println!("{:?}", ev);
+                    if ev == Event::Key(Key::Esc) {
+                        break;
+                    }
+                }
+                Err(io_err) => {
+                    println!("Error: {:?}", io_err);
+                    break;
+                }
             }
         }
-    }
+    });
+    executor.run().unwrap();
 
     // restore the old settings
     unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &old_term) };
diff --git a/term_input_futures/examples/test.rs b/term_input_futures/examples/test.rs
deleted file mode 100644
index 95b7b56a..00000000
--- a/term_input_futures/examples/test.rs
+++ /dev/null
@@ -1,73 +0,0 @@
-extern crate futures;
-extern crate libc;
-extern crate term_input_futures;
-extern crate tokio;
-
-use std::io;
-use std::io::Write;
-
-use futures::future;
-use futures::Future;
-use futures::Stream;
-
-use term_input_futures::{Event, Input, Key};
-
-enum IterErr {
-    Io(std::io::Error),
-    Break,
-}
-
-fn main() {
-    // put the terminal in non-buffering, no-enchoing mode
-    let mut old_term: libc::termios = unsafe { std::mem::zeroed() };
-    unsafe {
-        libc::tcgetattr(libc::STDIN_FILENO, &mut old_term);
-    }
-
-    let mut new_term: libc::termios = old_term.clone();
-    new_term.c_iflag &= !(libc::IGNBRK
-        | libc::BRKINT
-        | libc::PARMRK
-        | libc::ISTRIP
-        | libc::INLCR
-        | libc::IGNCR
-        | libc::ICRNL
-        | libc::IXON);
-    new_term.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN);
-    unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &new_term) };
-
-    // enable focus events
-    {
-        let stdout = io::stdout();
-        stdout.lock().write_all(b"\x1b[?1004h").unwrap();
-        stdout.lock().flush().unwrap();
-    }
-
-    /* DO THE BUSINESS HERE */
-    let input = Input::new();
-    tokio::run(future::lazy(|| {
-        input
-            .map_err(IterErr::Io)
-            .for_each(|ev| {
-                println!("{:?}", ev);
-                if ev == Event::Key(Key::Esc) {
-                    future::err(IterErr::Break)
-                } else {
-                    future::ok(())
-                }
-            })
-            .map_err(|e| match e {
-                IterErr::Break => {}
-                IterErr::Io(io_err) => println!("Error: {:?}", io_err),
-            })
-    }));
-
-    // restore the old settings
-    unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &old_term) };
-
-    // disable focus events
-    {
-        let stdout = io::stdout();
-        stdout.lock().write_all(b"\x1b[?1004l").unwrap();
-    }
-}
diff --git a/term_input_std_futures/examples/test.rs b/term_input_std_futures/examples/test.rs
deleted file mode 100644
index b5118d93..00000000
--- a/term_input_std_futures/examples/test.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-#![feature(async_closure)]
-
-extern crate libc;
-extern crate term_input_std_futures;
-extern crate tokio;
-
-use futures::stream::StreamExt;
-use std::io;
-use std::io::Write;
-
-use term_input_std_futures::{Event, Input, Key};
-
-fn main() {
-    // put the terminal in non-buffering, no-enchoing mode
-    let mut old_term: libc::termios = unsafe { std::mem::zeroed() };
-    unsafe {
-        libc::tcgetattr(libc::STDIN_FILENO, &mut old_term);
-    }
-
-    let mut new_term: libc::termios = old_term.clone();
-    new_term.c_iflag &= !(libc::IGNBRK
-        | libc::BRKINT
-        | libc::PARMRK
-        | libc::ISTRIP
-        | libc::INLCR
-        | libc::IGNCR
-        | libc::ICRNL
-        | libc::IXON);
-    new_term.c_lflag &= !(libc::ICANON | libc::ECHO | libc::ISIG | libc::IEXTEN);
-    unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &new_term) };
-
-    // enable focus events
-    {
-        let stdout = io::stdout();
-        stdout.lock().write_all(b"\x1b[?1004h").unwrap();
-        stdout.lock().flush().unwrap();
-    }
-
-    /* DO THE BUSINESS HERE */
-    let mut input = Input::new();
-    let mut executor = tokio::runtime::current_thread::Runtime::new().unwrap();
-    executor.spawn(async move {
-        while let Some(mb_ev) = input.next().await {
-            match mb_ev {
-                Ok(ev) => {
-                    println!("{:?}", ev);
-                    if ev == Event::Key(Key::Esc) {
-                        break;
-                    }
-                }
-                Err(io_err) => {
-                    println!("Error: {:?}", io_err);
-                    break;
-                }
-            }
-        }
-    });
-    executor.run().unwrap();
-
-    // restore the old settings
-    unsafe { libc::tcsetattr(libc::STDIN_FILENO, libc::TCSANOW, &old_term) };
-
-    // disable focus events
-    {
-        let stdout = io::stdout();
-        stdout.lock().write_all(b"\x1b[?1004l").unwrap();
-    }
-}