about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik Agaram <vc@akkartik.com>2019-12-07 16:36:40 -0800
committerKartik Agaram <vc@akkartik.com>2019-12-07 18:15:49 -0800
commitf821c0e28b5e9ae9c91758276acd10484f8388bc (patch)
tree3aba2f5e49d5a45bc328db7da077b7df527483f0
parent9e45cae061fd345d3270f236769bd94966a42eb2 (diff)
downloadmu-f821c0e28b5e9ae9c91758276acd10484f8388bc.tar.gz
5800 - move `browse_trace` to `tools/` dir
-rw-r--r--Readme.md6
-rwxr-xr-xbrowse_trace/browse_trace7
-rwxr-xr-xclean4
-rw-r--r--tools/Readme.md2
-rwxr-xr-xtools/browse_trace21
-rw-r--r--tools/browse_trace.cc (renamed from browse_trace/browse_trace.cc)554
-rw-r--r--tools/browse_trace.readme.md (renamed from browse_trace/Readme.md)4
-rw-r--r--tools/termbox/COPYING19
-rw-r--r--tools/termbox/Readme2
-rw-r--r--tools/termbox/bytebuffer.inl79
-rw-r--r--tools/termbox/input.inl185
-rw-r--r--tools/termbox/output.inl320
-rw-r--r--tools/termbox/termbox.c397
-rw-r--r--tools/termbox/termbox.h190
-rw-r--r--tools/termbox/utf8.c79
15 files changed, 1575 insertions, 294 deletions
diff --git a/Readme.md b/Readme.md
index 0a83bf17..8f3d6fe0 100644
--- a/Readme.md
+++ b/Readme.md
@@ -77,7 +77,7 @@ messages.
   42
   ```
 
-Emulated runs can generate a trace that permits [time-travel debugging](https://github.com/akkartik/mu/blob/master/browse_trace/Readme.md).
+Emulated runs can generate a trace that permits [time-travel debugging](https://github.com/akkartik/mu/blob/master/tools/browse_trace.readme.md).
 
   ```sh
   $ ./subx --debug translate init.linux examples/factorial.subx -o examples/factorial
@@ -87,7 +87,7 @@ Emulated runs can generate a trace that permits [time-travel debugging](https://
   $ ./subx --debug --trace run examples/factorial
   saving trace to 'last_run'
 
-  $ ./browse_trace/browse_trace last_run  # text-mode debugger UI
+  $ tools/browse_trace last_run  # text-mode debugger UI
   ```
 
 You can write tests for your programs. The entire stack is thoroughly covered
@@ -542,7 +542,7 @@ rudimentary but hopefully still workable toolkit:
   layer. It makes the trace a lot more verbose and a lot less dense, necessitating
   a lot more scrolling around, so I keep it turned off most of the time.
 
-* If the trace seems overwhelming, try [browsing it](https://github.com/akkartik/mu/blob/master/browse_trace/Readme.md)
+* If the trace seems overwhelming, try [browsing it](https://github.com/akkartik/mu/blob/master/tools/browse_trace.readme.md)
   in the 'time-travel debugger'.
 
 Hopefully these hints are enough to get you started. The main thing to
diff --git a/browse_trace/browse_trace b/browse_trace/browse_trace
deleted file mode 100755
index f80ea659..00000000
--- a/browse_trace/browse_trace
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/bin/sh
-set -e
-
-cd `dirname $0`
-./build
-cd -
-`dirname $0`/browse_trace_bin $*
diff --git a/clean b/clean
index ef6e9d4d..9eadbf7a 100755
--- a/clean
+++ b/clean
@@ -6,7 +6,7 @@ rm -rf subx.cc subx_bin* *_list
 rm -rf .until
 test $# -gt 0 && exit 0  # convenience: 'clean top-level' to leave subsidiary tools alone
 rm -rf tools/enumerate tangle/tangle tangle/*_list */*.dSYM
-rm -rf browse_trace/browse_trace_bin browse_trace/*_list
-rm -rf tools/treeshake tools/linkify tools/*.dSYM
+rm -rf tools/browse_trace_bin tools/treeshake tools/linkify tools/*.dSYM
+rm -rf tools/termbox/*.o tools/termbox/libtermbox.a
 rm -rf tmp_linux mu_linux.iso outfs initrd.fat mu_soso.iso
 ( cd kernel.soso  &&  make clean; )
diff --git a/tools/Readme.md b/tools/Readme.md
index 29a1a6ef..fd889ec5 100644
--- a/tools/Readme.md
+++ b/tools/Readme.md
@@ -12,6 +12,8 @@ These are built automatically.
 
 These are built lazily.
 
+* `browse_trace`: debugging tool. See `browse_trace.readme.md` for details.
+
 * `linkify`: inserts hyperlinks from variables to definitions in Mu's html
   sources. Hacky; just see the number of tests. Invoked by `update_html`.
 
diff --git a/tools/browse_trace b/tools/browse_trace
new file mode 100755
index 00000000..e68b541b
--- /dev/null
+++ b/tools/browse_trace
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -e
+
+test "$CXX" || export CXX=c++
+test "$CC" || export CC=cc
+test "$CFLAGS" || export CFLAGS="-g -O2"
+export CFLAGS="$CFLAGS -Wall -Wextra -ftrapv -fno-strict-aliasing"
+
+# build if doesn't exist
+[ ! -f `dirname $0`/browse_trace_bin ] && (
+  cd `dirname $0`
+  [ ! -f termbox/libtermbox.a ] && (
+    cd termbox
+    $CC $CFLAGS -c utf8.c
+    $CC $CFLAGS -c termbox.c
+    ar rcs libtermbox.a *.o
+  )
+  $CXX $CFLAGS browse_trace.cc termbox/libtermbox.a -o browse_trace_bin
+)
+
+`dirname $0`/browse_trace_bin $*
diff --git a/browse_trace/browse_trace.cc b/tools/browse_trace.cc
index 76e968a7..b15da9c8 100644
--- a/browse_trace/browse_trace.cc
+++ b/tools/browse_trace.cc
@@ -52,12 +52,6 @@ struct trace_stream {
 enum search_direction { FORWARD, BACKWARD };
 // End Types
 
-// Function prototypes are auto-generated in the 'build*' scripts; define your
-// functions in any order. Just be sure to declare each function header all on
-// one line, ending with the '{'. Our auto-generation scripts are too minimal
-// and simple-minded to handle anything else.
-#include "function_list"  // by convention, files ending with '_list' are auto-generated
-
 // from http://stackoverflow.com/questions/152643/idiomatic-c-for-reading-from-a-const-map
 template<typename T> typename T::mapped_type& get(T& map, typename T::key_type const& key) {
   typename T::iterator iter(map.find(key));
@@ -101,6 +95,280 @@ bool has_data(istream& in) {
   return in && !in.eof();
 }
 
+void skip_whitespace_but_not_newline(istream& in) {
+  while (true) {
+    if (!has_data(in)) break;
+    else if (in.peek() == '\n') break;
+    else if (isspace(in.peek())) in.get();
+    else break;
+  }
+}
+
+void load_trace(const char* filename) {
+  ifstream tin(filename);
+  if (!tin) {
+    cerr << "no such file: " << filename << '\n';
+    exit(1);
+  }
+  Trace_stream = new trace_stream;
+  while (has_data(tin)) {
+    tin >> std::noskipws;
+      skip_whitespace_but_not_newline(tin);
+      if (!isdigit(tin.peek())) {
+        string dummy;
+        getline(tin, dummy);
+        continue;
+      }
+    tin >> std::skipws;
+    int depth;
+    tin >> depth;
+    string label;
+    tin >> label;
+    if (*--label.end() == ':') label.erase(--label.end());
+    string line;
+    getline(tin, line);
+    Trace_stream->past_lines.push_back(trace_line(line, label, depth));
+  }
+  cerr << "lines read: " << Trace_stream->past_lines.size() << '\n';
+}
+
+// update Trace_indices for each screen_row on the basis of Top_of_screen and Visible
+void refresh_screen_rows() {
+  int screen_row = 0, index = 0;
+  Trace_index.clear();
+  for (screen_row = 0, index = Top_of_screen;  screen_row < tb_height() && index < SIZE(Trace_stream->past_lines);  ++screen_row, ++index) {
+    // skip lines without depth for now
+    while (!contains_key(Visible, index)) {
+      ++index;
+      if (index >= SIZE(Trace_stream->past_lines)) goto done;
+    }
+    assert(index < SIZE(Trace_stream->past_lines));
+    put(Trace_index, screen_row, index);
+  }
+done:;
+}
+
+void clear_line(int screen_row) {
+  tb_set_cursor(0, screen_row);
+  for (int col = 0;  col < tb_width();  ++col)
+    tb_print(' ', TB_WHITE, TB_BLACK);
+  tb_set_cursor(0, screen_row);
+}
+
+int read_key() {
+  tb_event event;
+  do {
+    tb_poll_event(&event);
+  } while (event.type != TB_EVENT_KEY);
+  return event.key ? event.key : event.ch;
+}
+
+int lines_hidden(int screen_row) {
+  assert(contains_key(Trace_index, screen_row));
+  if (!contains_key(Trace_index, screen_row+1))
+    return SIZE(Trace_stream->past_lines) - get(Trace_index, screen_row);
+  else
+    return get(Trace_index, screen_row+1) - get(Trace_index, screen_row);
+}
+
+bool in_range(const vector<pair<size_t, size_t> >& highlight_ranges, size_t idx) {
+  for (int i = 0;  i < SIZE(highlight_ranges);  ++i) {
+    if (idx >= highlight_ranges.at(i).first && idx < highlight_ranges.at(i).second)
+      return true;
+    if (idx < highlight_ranges.at(i).second) break;
+  }
+  return false;
+}
+
+vector<pair<size_t, size_t> > find_all_occurrences(const string& s, const string& pat) {
+  vector<pair<size_t, size_t> > result;
+  if (pat.empty()) return result;
+  size_t idx = 0;
+  while (true) {
+    size_t next_idx = s.find(pat, idx);
+    if (next_idx == string::npos) break;
+    result.push_back(pair<size_t, size_t>(next_idx, next_idx+SIZE(pat)));
+    idx = next_idx+SIZE(pat);
+  }
+  return result;
+}
+
+void render_line(int screen_row, const string& s, bool cursor_line) {
+  int col = 0;
+  int color = TB_WHITE;
+  int background_color = cursor_line ? /*subtle grey*/240 : TB_BLACK;
+  vector<pair<size_t, size_t> > highlight_ranges = find_all_occurrences(s, Current_search_pattern);
+  tb_set_cursor(0, screen_row);
+  for (col = 0;  col < tb_width() && col+Left_of_screen < SIZE(s);  ++col) {
+    char c = s.at(col+Left_of_screen);  // todo: unicode
+    if (c == '\n') c = ';';  // replace newlines with semi-colons
+    // escapes. hack: can't start a line with them.
+    if (c == '\1') { color = /*red*/1;  continue; }
+    if (c == '\2') { color = TB_WHITE;  continue; }
+    if (in_range(highlight_ranges, col+Left_of_screen))
+      tb_print(c, TB_BLACK, /*yellow*/11);
+    else
+      tb_print(c, color, background_color);
+  }
+  for (;  col < tb_width();  ++col)
+    tb_print(' ', TB_WHITE, background_color);
+}
+
+void search_next(const string& pat) {
+  for (int trace_index = get(Trace_index, Display_row)+1;  trace_index < SIZE(Trace_stream->past_lines);  ++trace_index) {
+    if (!contains_key(Visible, trace_index)) continue;
+    const trace_line& line = Trace_stream->past_lines.at(trace_index);
+    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
+    Top_of_screen = trace_index;
+    Display_row = 0;
+    refresh_screen_rows();
+    return;
+  }
+}
+
+void search_previous(const string& pat) {
+  for (int trace_index = get(Trace_index, Display_row)-1;  trace_index >= 0;  --trace_index) {
+    if (!contains_key(Visible, trace_index)) continue;
+    const trace_line& line = Trace_stream->past_lines.at(trace_index);
+    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
+    Top_of_screen = trace_index;
+    Display_row = 0;
+    refresh_screen_rows();
+    return;
+  }
+}
+
+void search(const string& pat, search_direction dir) {
+  if (dir == FORWARD) search_next(pat);
+  else search_previous(pat);
+}
+
+search_direction opposite(search_direction dir) {
+  if (dir == FORWARD) return BACKWARD;
+  else return FORWARD;
+}
+
+bool start_search_editor(search_direction dir) {
+  const int bottom_screen_line = tb_height()-1;
+  // run a little editor just in the last line of the screen
+  clear_line(bottom_screen_line);
+  int col = 0;  // screen column of cursor on bottom line. also used to update pattern.
+  tb_set_cursor(col, bottom_screen_line);
+  tb_print('/', TB_WHITE, TB_BLACK);
+  ++col;
+  string pattern;
+  while (true) {
+    int key = read_key();
+    if (key == TB_KEY_ENTER) {
+      if (!pattern.empty()) {
+        Current_search_pattern = pattern;
+        Current_search_direction = dir;
+      }
+      return true;
+    }
+    else if (key == TB_KEY_ESC || key == TB_KEY_CTRL_C) {
+      return false;
+    }
+    else if (key == TB_KEY_ARROW_LEFT) {
+      if (col > /*slash*/1) {
+        --col;
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_ARROW_RIGHT) {
+      if (col-/*slash*/1 < SIZE(pattern)) {
+        ++col;
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_HOME || key == TB_KEY_CTRL_A) {
+      col = /*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_END || key == TB_KEY_CTRL_E) {
+      col = SIZE(pattern)+/*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
+      if (col > /*slash*/1) {
+        assert(col <= SIZE(pattern)+1);
+        --col;
+        // update pattern
+        pattern.erase(col-/*slash*/1, /*len*/1);
+        // update screen
+        tb_set_cursor(col, bottom_screen_line);
+        for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+          tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+        tb_print(' ', TB_WHITE, TB_BLACK);
+        tb_set_cursor(col, bottom_screen_line);
+      }
+    }
+    else if (key == TB_KEY_CTRL_K) {
+      int old_pattern_size = SIZE(pattern);
+      pattern.erase(col-/*slash*/1, SIZE(pattern) - (col-/*slash*/1));
+      tb_set_cursor(col, bottom_screen_line);
+      for (int x = col;  x < old_pattern_size+/*slash*/1;  ++x)
+        tb_print(' ', TB_WHITE, TB_BLACK);
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key == TB_KEY_CTRL_U) {
+      int old_pattern_size = SIZE(pattern);
+      pattern.erase(0, col-/*slash*/1);
+      col = /*skip slash*/1;
+      tb_set_cursor(col, bottom_screen_line);
+      for (int x = /*slash*/1;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+      for (int x = SIZE(pattern)+/*slash*/1;  x < old_pattern_size+/*skip slash*/1;  ++x)
+        tb_print(' ', TB_WHITE, TB_BLACK);
+      tb_set_cursor(col, bottom_screen_line);
+    }
+    else if (key < 128) {  // ascii only
+      // update pattern
+      char c = static_cast<char>(key);
+      assert(col-1 >= 0);
+      assert(col-1 <= SIZE(pattern));
+      pattern.insert(col-/*slash*/1, /*num*/1, c);
+      // update screen
+      for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
+        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
+      ++col;
+      tb_set_cursor(col, bottom_screen_line);
+    }
+  }
+}
+
+void render() {
+  int screen_row = 0;
+  for (screen_row = 0;  screen_row < tb_height();  ++screen_row) {
+    if (!contains_key(Trace_index, screen_row)) break;
+    trace_line& curr_line = Trace_stream->past_lines.at(get(Trace_index, screen_row));
+    ostringstream out;
+    if (screen_row < tb_height()-1) {
+      int delta = lines_hidden(screen_row);
+      // home-brew escape sequence for red
+      if (delta > 1) {
+        if (delta > 999) out << static_cast<char>(1);
+        out << std::setw(6) << delta << "| ";
+        if (delta > 999) out << static_cast<char>(2);
+      }
+      else {
+        out << "        ";
+      }
+    }
+    else {
+      out << "        ";
+    }
+    out << std::setw(4) << curr_line.depth << ' ' << curr_line.label << ": " << curr_line.contents;
+    render_line(screen_row, out.str(), screen_row == Display_row);
+  }
+  // clear rest of screen
+  Last_printed_row = screen_row-1;
+  for (;  screen_row < tb_height();  ++screen_row)
+    render_line(screen_row, "~", /*cursor_line?*/false);
+  // move cursor back to display row at the end
+  tb_set_cursor(0, Display_row);
+}
+
 int main(int argc, char* argv[]) {
   if (argc != 2) {
     cerr << "Usage: browse_trace <trace file>\n";
@@ -277,277 +545,3 @@ int main(int argc, char* argv[]) {
   tb_shutdown();
   return 0;
 }
-
-bool start_search_editor(search_direction dir) {
-  const int bottom_screen_line = tb_height()-1;
-  // run a little editor just in the last line of the screen
-  clear_line(bottom_screen_line);
-  int col = 0;  // screen column of cursor on bottom line. also used to update pattern.
-  tb_set_cursor(col, bottom_screen_line);
-  tb_print('/', TB_WHITE, TB_BLACK);
-  ++col;
-  string pattern;
-  while (true) {
-    int key = read_key();
-    if (key == TB_KEY_ENTER) {
-      if (!pattern.empty()) {
-        Current_search_pattern = pattern;
-        Current_search_direction = dir;
-      }
-      return true;
-    }
-    else if (key == TB_KEY_ESC || key == TB_KEY_CTRL_C) {
-      return false;
-    }
-    else if (key == TB_KEY_ARROW_LEFT) {
-      if (col > /*slash*/1) {
-        --col;
-        tb_set_cursor(col, bottom_screen_line);
-      }
-    }
-    else if (key == TB_KEY_ARROW_RIGHT) {
-      if (col-/*slash*/1 < SIZE(pattern)) {
-        ++col;
-        tb_set_cursor(col, bottom_screen_line);
-      }
-    }
-    else if (key == TB_KEY_HOME || key == TB_KEY_CTRL_A) {
-      col = /*skip slash*/1;
-      tb_set_cursor(col, bottom_screen_line);
-    }
-    else if (key == TB_KEY_END || key == TB_KEY_CTRL_E) {
-      col = SIZE(pattern)+/*skip slash*/1;
-      tb_set_cursor(col, bottom_screen_line);
-    }
-    else if (key == TB_KEY_BACKSPACE || key == TB_KEY_BACKSPACE2) {
-      if (col > /*slash*/1) {
-        assert(col <= SIZE(pattern)+1);
-        --col;
-        // update pattern
-        pattern.erase(col-/*slash*/1, /*len*/1);
-        // update screen
-        tb_set_cursor(col, bottom_screen_line);
-        for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
-          tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
-        tb_print(' ', TB_WHITE, TB_BLACK);
-        tb_set_cursor(col, bottom_screen_line);
-      }
-    }
-    else if (key == TB_KEY_CTRL_K) {
-      int old_pattern_size = SIZE(pattern);
-      pattern.erase(col-/*slash*/1, SIZE(pattern) - (col-/*slash*/1));
-      tb_set_cursor(col, bottom_screen_line);
-      for (int x = col;  x < old_pattern_size+/*slash*/1;  ++x)
-        tb_print(' ', TB_WHITE, TB_BLACK);
-      tb_set_cursor(col, bottom_screen_line);
-    }
-    else if (key == TB_KEY_CTRL_U) {
-      int old_pattern_size = SIZE(pattern);
-      pattern.erase(0, col-/*slash*/1);
-      col = /*skip slash*/1;
-      tb_set_cursor(col, bottom_screen_line);
-      for (int x = /*slash*/1;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
-        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
-      for (int x = SIZE(pattern)+/*slash*/1;  x < old_pattern_size+/*skip slash*/1;  ++x)
-        tb_print(' ', TB_WHITE, TB_BLACK);
-      tb_set_cursor(col, bottom_screen_line);
-    }
-    else if (key < 128) {  // ascii only
-      // update pattern
-      char c = static_cast<char>(key);
-      assert(col-1 >= 0);
-      assert(col-1 <= SIZE(pattern));
-      pattern.insert(col-/*slash*/1, /*num*/1, c);
-      // update screen
-      for (int x = col;  x < SIZE(pattern)+/*skip slash*/1;  ++x)
-        tb_print(pattern.at(x-/*slash*/1), TB_WHITE, TB_BLACK);
-      ++col;
-      tb_set_cursor(col, bottom_screen_line);
-    }
-  }
-}
-
-void search(const string& pat, search_direction dir) {
-  if (dir == FORWARD) search_next(pat);
-  else search_previous(pat);
-}
-
-search_direction opposite(search_direction dir) {
-  if (dir == FORWARD) return BACKWARD;
-  else return FORWARD;
-}
-
-void search_next(const string& pat) {
-  for (int trace_index = get(Trace_index, Display_row)+1;  trace_index < SIZE(Trace_stream->past_lines);  ++trace_index) {
-    if (!contains_key(Visible, trace_index)) continue;
-    const trace_line& line = Trace_stream->past_lines.at(trace_index);
-    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
-    Top_of_screen = trace_index;
-    Display_row = 0;
-    refresh_screen_rows();
-    return;
-  }
-}
-
-void search_previous(const string& pat) {
-  for (int trace_index = get(Trace_index, Display_row)-1;  trace_index >= 0;  --trace_index) {
-    if (!contains_key(Visible, trace_index)) continue;
-    const trace_line& line = Trace_stream->past_lines.at(trace_index);
-    if (line.label.find(pat) == string::npos && line.contents.find(pat) == string::npos) continue;
-    Top_of_screen = trace_index;
-    Display_row = 0;
-    refresh_screen_rows();
-    return;
-  }
-}
-
-void clear_line(int screen_row) {
-  tb_set_cursor(0, screen_row);
-  for (int col = 0;  col < tb_width();  ++col)
-    tb_print(' ', TB_WHITE, TB_BLACK);
-  tb_set_cursor(0, screen_row);
-}
-
-// update Trace_indices for each screen_row on the basis of Top_of_screen and Visible
-void refresh_screen_rows() {
-  int screen_row = 0, index = 0;
-  Trace_index.clear();
-  for (screen_row = 0, index = Top_of_screen;  screen_row < tb_height() && index < SIZE(Trace_stream->past_lines);  ++screen_row, ++index) {
-    // skip lines without depth for now
-    while (!contains_key(Visible, index)) {
-      ++index;
-      if (index >= SIZE(Trace_stream->past_lines)) goto done;
-    }
-    assert(index < SIZE(Trace_stream->past_lines));
-    put(Trace_index, screen_row, index);
-  }
-done:;
-}
-
-void render() {
-  int screen_row = 0;
-  for (screen_row = 0;  screen_row < tb_height();  ++screen_row) {
-    if (!contains_key(Trace_index, screen_row)) break;
-    trace_line& curr_line = Trace_stream->past_lines.at(get(Trace_index, screen_row));
-    ostringstream out;
-    if (screen_row < tb_height()-1) {
-      int delta = lines_hidden(screen_row);
-      // home-brew escape sequence for red
-      if (delta > 1) {
-        if (delta > 999) out << static_cast<char>(1);
-        out << std::setw(6) << delta << "| ";
-        if (delta > 999) out << static_cast<char>(2);
-      }
-      else {
-        out << "        ";
-      }
-    }
-    else {
-      out << "        ";
-    }
-    out << std::setw(4) << curr_line.depth << ' ' << curr_line.label << ": " << curr_line.contents;
-    render_line(screen_row, out.str(), screen_row == Display_row);
-  }
-  // clear rest of screen
-  Last_printed_row = screen_row-1;
-  for (;  screen_row < tb_height();  ++screen_row)
-    render_line(screen_row, "~", /*cursor_line?*/false);
-  // move cursor back to display row at the end
-  tb_set_cursor(0, Display_row);
-}
-
-int lines_hidden(int screen_row) {
-  assert(contains_key(Trace_index, screen_row));
-  if (!contains_key(Trace_index, screen_row+1))
-    return SIZE(Trace_stream->past_lines) - get(Trace_index, screen_row);
-  else
-    return get(Trace_index, screen_row+1) - get(Trace_index, screen_row);
-}
-
-void render_line(int screen_row, const string& s, bool cursor_line) {
-  int col = 0;
-  int color = TB_WHITE;
-  int background_color = cursor_line ? /*subtle grey*/240 : TB_BLACK;
-  vector<pair<size_t, size_t> > highlight_ranges = find_all_occurrences(s, Current_search_pattern);
-  tb_set_cursor(0, screen_row);
-  for (col = 0;  col < tb_width() && col+Left_of_screen < SIZE(s);  ++col) {
-    char c = s.at(col+Left_of_screen);  // todo: unicode
-    if (c == '\n') c = ';';  // replace newlines with semi-colons
-    // escapes. hack: can't start a line with them.
-    if (c == '\1') { color = /*red*/1;  continue; }
-    if (c == '\2') { color = TB_WHITE;  continue; }
-    if (in_range(highlight_ranges, col+Left_of_screen))
-      tb_print(c, TB_BLACK, /*yellow*/11);
-    else
-      tb_print(c, color, background_color);
-  }
-  for (;  col < tb_width();  ++col)
-    tb_print(' ', TB_WHITE, background_color);
-}
-
-vector<pair<size_t, size_t> > find_all_occurrences(const string& s, const string& pat) {
-  vector<pair<size_t, size_t> > result;
-  if (pat.empty()) return result;
-  size_t idx = 0;
-  while (true) {
-    size_t next_idx = s.find(pat, idx);
-    if (next_idx == string::npos) break;
-    result.push_back(pair<size_t, size_t>(next_idx, next_idx+SIZE(pat)));
-    idx = next_idx+SIZE(pat);
-  }
-  return result;
-}
-
-bool in_range(const vector<pair<size_t, size_t> >& highlight_ranges, size_t idx) {
-  for (int i = 0;  i < SIZE(highlight_ranges);  ++i) {
-    if (idx >= highlight_ranges.at(i).first && idx < highlight_ranges.at(i).second)
-      return true;
-    if (idx < highlight_ranges.at(i).second) break;
-  }
-  return false;
-}
-
-void load_trace(const char* filename) {
-  ifstream tin(filename);
-  if (!tin) {
-    cerr << "no such file: " << filename << '\n';
-    exit(1);
-  }
-  Trace_stream = new trace_stream;
-  while (has_data(tin)) {
-    tin >> std::noskipws;
-      skip_whitespace_but_not_newline(tin);
-      if (!isdigit(tin.peek())) {
-        string dummy;
-        getline(tin, dummy);
-        continue;
-      }
-    tin >> std::skipws;
-    int depth;
-    tin >> depth;
-    string label;
-    tin >> label;
-    if (*--label.end() == ':') label.erase(--label.end());
-    string line;
-    getline(tin, line);
-    Trace_stream->past_lines.push_back(trace_line(line, label, depth));
-  }
-  cerr << "lines read: " << Trace_stream->past_lines.size() << '\n';
-}
-
-int read_key() {
-  tb_event event;
-  do {
-    tb_poll_event(&event);
-  } while (event.type != TB_EVENT_KEY);
-  return event.key ? event.key : event.ch;
-}
-
-void skip_whitespace_but_not_newline(istream& in) {
-  while (true) {
-    if (!has_data(in)) break;
-    else if (in.peek() == '\n') break;
-    else if (isspace(in.peek())) in.get();
-    else break;
-  }
-}
diff --git a/browse_trace/Readme.md b/tools/browse_trace.readme.md
index 395058f1..d607095e 100644
--- a/browse_trace/Readme.md
+++ b/tools/browse_trace.readme.md
@@ -4,7 +4,7 @@ To try it out, first create an example trace (from the top-level `mu/`
 directory):
 
   ```shell
-  ./mu --trace nqueens.mu
+  ./subx --trace run apps/factorial
   ```
 
 This command will save a trace of its execution in a file called `last_run`.
@@ -14,7 +14,7 @@ and a single-word 'label', followed by a colon and whitespace.
 Now browse this trace:
 
   ```shell
-  ./browse_trace/browse_trace last_run
+  tools/browse_trace last_run
   ```
 
 You should now find yourself in a UI showing a subsequence of lines from the
diff --git a/tools/termbox/COPYING b/tools/termbox/COPYING
new file mode 100644
index 00000000..e9bb4eac
--- /dev/null
+++ b/tools/termbox/COPYING
@@ -0,0 +1,19 @@
+Copyright (C) 2010-2013 nsf <no.smile.face@gmail.com>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/tools/termbox/Readme b/tools/termbox/Readme
new file mode 100644
index 00000000..d97cae4e
--- /dev/null
+++ b/tools/termbox/Readme
@@ -0,0 +1,2 @@
+Fork of https://github.com/nsf/termbox as of 2015-06-05
+git hash 252bef01264a2aeef22ebe5bae0b5893a58947f3
diff --git a/tools/termbox/bytebuffer.inl b/tools/termbox/bytebuffer.inl
new file mode 100644
index 00000000..aae8f073
--- /dev/null
+++ b/tools/termbox/bytebuffer.inl
@@ -0,0 +1,79 @@
+struct bytebuffer {
+  char *buf;
+  int len;
+  int cap;
+};
+
+static void bytebuffer_reserve(struct bytebuffer *b, int cap) {
+  if (b->cap >= cap) {
+    return;
+  }
+
+  // prefer doubling capacity
+  if (b->cap * 2 >= cap) {
+    cap = b->cap * 2;
+  }
+
+  char *newbuf = malloc(cap);
+  if (b->len > 0) {
+    // copy what was there, b->len > 0 assumes b->buf != null
+    memcpy(newbuf, b->buf, b->len);
+  }
+  if (b->buf) {
+    // in case there was an allocated buffer, free it
+    free(b->buf);
+  }
+  b->buf = newbuf;
+  b->cap = cap;
+}
+
+static void bytebuffer_init(struct bytebuffer *b, int cap) {
+  b->cap = 0;
+  b->len = 0;
+  b->buf = 0;
+
+  if (cap > 0) {
+    b->cap = cap;
+    b->buf = malloc(cap); // just assume malloc works always
+  }
+}
+
+static void bytebuffer_free(struct bytebuffer *b) {
+  if (b->buf)
+    free(b->buf);
+}
+
+static void bytebuffer_clear(struct bytebuffer *b) {
+  b->len = 0;
+}
+
+static void bytebuffer_append(struct bytebuffer *b, const char *data, int len) {
+  bytebuffer_reserve(b, b->len + len);
+  memcpy(b->buf + b->len, data, len);
+  b->len += len;
+}
+
+static void bytebuffer_puts(struct bytebuffer *b, const char *str) {
+  bytebuffer_append(b, str, strlen(str));
+}
+
+static void bytebuffer_resize(struct bytebuffer *b, int len) {
+  bytebuffer_reserve(b, len);
+  b->len = len;
+}
+
+static void bytebuffer_flush(struct bytebuffer *b, int fd) {
+  int yyy = write(fd, b->buf, b->len);
+  (void) yyy;
+  bytebuffer_clear(b);
+}
+
+static void bytebuffer_truncate(struct bytebuffer *b, int n) {
+  if (n <= 0)
+    return;
+  if (n > b->len)
+    n = b->len;
+  const int nmove = b->len - n;
+  memmove(b->buf, b->buf+n, nmove);
+  b->len -= n;
+}
diff --git a/tools/termbox/input.inl b/tools/termbox/input.inl
new file mode 100644
index 00000000..83b4bb8c
--- /dev/null
+++ b/tools/termbox/input.inl
@@ -0,0 +1,185 @@
+// if s1 starts with s2 returns true, else false
+// len is the length of s1
+// s2 should be null-terminated
+static bool starts_with(const char *s1, int len, const char *s2)
+{
+  int n = 0;
+  while (*s2 && n < len) {
+    if (*s1++ != *s2++)
+      return false;
+    n++;
+  }
+  return *s2 == 0;
+}
+
+#define FOO(...) { \
+  FILE* f = fopen("log", "a+"); \
+  fprintf(f, __VA_ARGS__); \
+  fclose(f); \
+}
+
+// convert escape sequence to event, and return consumed bytes on success (failure == 0)
+static int parse_escape_seq(struct tb_event *event, const char *buf, int len)
+{
+  static int parse_attempts = 0;
+  static const int MAX_PARSE_ATTEMPTS = 2;
+
+//?   int x = 0;
+//?   FOO("-- %d\n", len);
+//?   for (x = 0; x < len; ++x) {
+//?     FOO("%d\n", (unsigned char)buf[x]);
+//?   }
+  if (len >= 6 && starts_with(buf, len, "\033[M")) {
+
+    switch (buf[3] & 3) {
+    case 0:
+      if (buf[3] == 0x60)
+        event->key = TB_KEY_MOUSE_WHEEL_UP;
+      else
+        event->key = TB_KEY_MOUSE_LEFT;
+      break;
+    case 1:
+      if (buf[3] == 0x61)
+        event->key = TB_KEY_MOUSE_WHEEL_DOWN;
+      else
+        event->key = TB_KEY_MOUSE_MIDDLE;
+      break;
+    case 2:
+      event->key = TB_KEY_MOUSE_RIGHT;
+      break;
+    case 3:
+      event->key = TB_KEY_MOUSE_RELEASE;
+      break;
+    default:
+      parse_attempts = 0;
+      return -6;
+    }
+    event->type = TB_EVENT_MOUSE; // TB_EVENT_KEY by default
+
+    // the coord is 1,1 for upper left
+    event->x = (uint8_t)buf[4] - 1 - 32;
+    event->y = (uint8_t)buf[5] - 1 - 32;
+
+    parse_attempts = 0;
+    return 6;
+  }
+
+  // it's pretty simple here, find 'starts_with' match and return
+  // success, else return failure
+  int i;
+  for (i = 0; keys[i]; i++) {
+    if (starts_with(buf, len, keys[i])) {
+      event->ch = 0;
+      event->key = 0xFFFF-i;
+      parse_attempts = 0;
+      return strlen(keys[i]);
+    }
+  }
+
+  if (starts_with(buf, len, "\033[200~")) {
+    event->ch = 0;
+    event->key = TB_KEY_START_PASTE;
+    parse_attempts = 0;
+    return strlen("\033[200~");
+  }
+  if (starts_with(buf, len, "\033[201~")) {
+    event->ch = 0;
+    event->key = TB_KEY_END_PASTE;
+    parse_attempts = 0;
+    return strlen("\033[201~");
+  }
+  if (starts_with(buf, len, "\033[1;5A")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_UP;
+    parse_attempts = 0;
+    return strlen("\033[1;5A");
+  }
+  if (starts_with(buf, len, "\033[1;5B")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_DOWN;
+    parse_attempts = 0;
+    return strlen("\033[1;5B");
+  }
+  if (starts_with(buf, len, "\033[1;5C")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_RIGHT;
+    parse_attempts = 0;
+    return strlen("\033[1;5C");
+  }
+  if (starts_with(buf, len, "\033[1;5D")) {
+    event->ch = 0;
+    event->key = TB_KEY_CTRL_ARROW_LEFT;
+    parse_attempts = 0;
+    return strlen("\033[1;5D");
+  }
+  if (starts_with(buf, len, "\033[Z")) {
+    event->ch = 0;
+    event->key = TB_KEY_SHIFT_TAB;
+    parse_attempts = 0;
+    return strlen("\033[Z");
+  }
+
+  // no escape sequence recognized? wait a bit in case our buffer is incomplete
+  ++parse_attempts;
+  if (parse_attempts < MAX_PARSE_ATTEMPTS) return 0;
+  // still nothing? give up and consume just the esc
+  event->ch = 0;
+  event->key = TB_KEY_ESC;
+  parse_attempts = 0;
+  return 1;
+}
+
+static bool extract_event(struct tb_event *event, struct bytebuffer *inbuf)
+{
+  const char *buf = inbuf->buf;
+  const int len = inbuf->len;
+  if (len == 0)
+    return false;
+
+//?   int x = 0;
+//?   FOO("== %d\n", len);
+//?   for (x = 0; x < len; ++x) {
+//?     FOO("%x\n", (unsigned char)buf[x]);
+//?   }
+  if (buf[0] == '\033') {
+    int n = parse_escape_seq(event, buf, len);
+    if (n == 0) return false;
+//?     FOO("parsed: %u %u %u %u\n", n, (unsigned int)event->type, (unsigned int)event->key, event->ch);
+    bool success = true;
+    if (n < 0) {
+      success = false;
+      n = -n;
+    }
+    bytebuffer_truncate(inbuf, n);
+    return success;
+  }
+
+  // if we're here, this is not an escape sequence and not an alt sequence
+  // so, it's a FUNCTIONAL KEY or a UNICODE character
+
+  // first of all check if it's a functional key
+  if ((unsigned char)buf[0] <= TB_KEY_SPACE ||
+      (unsigned char)buf[0] == TB_KEY_BACKSPACE2)
+  {
+    // fill event, pop buffer, return success */
+    event->ch = 0;
+    event->key = (uint16_t)buf[0];
+    bytebuffer_truncate(inbuf, 1);
+    return true;
+  }
+
+  // feh... we got utf8 here
+
+  // check if there is all bytes
+  if (len >= tb_utf8_char_length(buf[0])) {
+    /* everything ok, fill event, pop buffer, return success */
+    tb_utf8_char_to_unicode(&event->ch, buf);
+    event->key = 0;
+    bytebuffer_truncate(inbuf, tb_utf8_char_length(buf[0]));
+    return true;
+  }
+
+  // event isn't recognized, perhaps there is not enough bytes in utf8
+  // sequence
+  return false;
+}
diff --git a/tools/termbox/output.inl b/tools/termbox/output.inl
new file mode 100644
index 00000000..d87049ef
--- /dev/null
+++ b/tools/termbox/output.inl
@@ -0,0 +1,320 @@
+enum {
+  T_ENTER_CA,
+  T_EXIT_CA,
+  T_SHOW_CURSOR,
+  T_HIDE_CURSOR,
+  T_CLEAR_SCREEN,
+  T_SGR0,
+  T_UNDERLINE,
+  T_BOLD,
+  T_BLINK,
+  T_REVERSE,
+  T_ENTER_KEYPAD,
+  T_EXIT_KEYPAD,
+  T_ENTER_MOUSE,
+  T_EXIT_MOUSE,
+  T_ENTER_BRACKETED_PASTE,
+  T_EXIT_BRACKETED_PASTE,
+  T_FUNCS_NUM,
+};
+
+#define EUNSUPPORTED_TERM -1
+
+// rxvt-256color
+static const char *rxvt_256color_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *rxvt_256color_funcs[] = {
+  "\0337\033[?47h", "\033[2J\033[?47l\0338", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033=", "\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// Eterm
+static const char *eterm_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *eterm_funcs[] = {
+  "\0337\033[?47h", "\033[2J\033[?47l\0338", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "", "", "", "", "", "",
+};
+
+// screen
+static const char *screen_keys[] = {
+  "\033OP", "\033OQ", "\033OR", "\033OS", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[1~", "\033[4~", "\033[5~", "\033[6~", "\033OA", "\033OB", "\033OD", "\033OC", 0
+};
+static const char *screen_funcs[] = {
+  "\033[?1049h", "\033[?1049l", "\033[34h\033[?25h", "\033[?25l", "\033[H\033[J", "\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033[?1h\033=", "\033[?1l\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// rxvt-unicode
+static const char *rxvt_unicode_keys[] = {
+  "\033[11~", "\033[12~", "\033[13~", "\033[14~", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[7~", "\033[8~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *rxvt_unicode_funcs[] = {
+  "\033[?1049h", "\033[r\033[?1049l", "\033[?25h", "\033[?25l", "\033[H\033[2J", "\033[m\033(B", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033=", "\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+// linux
+static const char *linux_keys[] = {
+  "\033[[A", "\033[[B", "\033[[C", "\033[[D", "\033[[E", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033[1~", "\033[4~", "\033[5~", "\033[6~", "\033[A", "\033[B", "\033[D", "\033[C", 0
+};
+static const char *linux_funcs[] = {
+  "", "", "\033[?25h\033[?0c", "\033[?25l\033[?1c", "\033[H\033[J", "\033[0;10m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "", "", "", "", "", "",
+};
+
+// xterm
+static const char *xterm_keys[] = {
+  "\033OP", "\033OQ", "\033OR", "\033OS", "\033[15~", "\033[17~", "\033[18~", "\033[19~", "\033[20~", "\033[21~", "\033[23~", "\033[24~", "\033[2~", "\033[3~", "\033OH", "\033OF", "\033[5~", "\033[6~", "\033OA", "\033OB", "\033OD", "\033OC", 0
+};
+static const char *xterm_funcs[] = {
+  "\033[?1049h", "\033[?1049l", "\033[?12l\033[?25h", "\033[?25l", "\033[H\033[2J", "\033(B\033[m", "\033[4m", "\033[1m", "\033[5m", "\033[7m", "\033[?1h\033=", "\033[?1l\033>", "\033[?1000h", "\033[?1000l", "\033[?2004h", "\033[?2004l",
+};
+
+static struct term {
+  const char *name;
+  const char **keys;
+  const char **funcs;
+} terms[] = {
+  {"rxvt-256color", rxvt_256color_keys, rxvt_256color_funcs},
+  {"Eterm", eterm_keys, eterm_funcs},
+  {"screen", screen_keys, screen_funcs},
+  {"rxvt-unicode", rxvt_unicode_keys, rxvt_unicode_funcs},
+  {"linux", linux_keys, linux_funcs},
+  {"xterm", xterm_keys, xterm_funcs},
+  {0, 0, 0},
+};
+
+static bool init_from_terminfo = false;
+static const char **keys;
+static const char **funcs;
+
+static int try_compatible(const char *term, const char *name,
+        const char **tkeys, const char **tfuncs)
+{
+  if (strstr(term, name)) {
+    keys = tkeys;
+    funcs = tfuncs;
+    return 0;
+  }
+
+  return EUNSUPPORTED_TERM;
+}
+
+static int init_term_builtin(void)
+{
+  int i;
+  const char *term = getenv("TERM");
+
+  if (term) {
+    for (i = 0; terms[i].name; i++) {
+      if (!strcmp(terms[i].name, term)) {
+        keys = terms[i].keys;
+        funcs = terms[i].funcs;
+        return 0;
+      }
+    }
+
+    /* let's do some heuristic, maybe it's a compatible terminal */
+    if (try_compatible(term, "xterm", xterm_keys, xterm_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "rxvt", rxvt_unicode_keys, rxvt_unicode_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "linux", linux_keys, linux_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "Eterm", eterm_keys, eterm_funcs) == 0)
+      return 0;
+    if (try_compatible(term, "screen", screen_keys, screen_funcs) == 0)
+      return 0;
+    /* let's assume that 'cygwin' is xterm compatible */
+    if (try_compatible(term, "cygwin", xterm_keys, xterm_funcs) == 0)
+      return 0;
+  }
+
+  return EUNSUPPORTED_TERM;
+}
+
+//----------------------------------------------------------------------
+// terminfo
+//----------------------------------------------------------------------
+
+static char *read_file(const char *file) {
+  FILE *f = fopen(file, "rb");
+  if (!f)
+    return 0;
+
+  struct stat st;
+  if (fstat(fileno(f), &st) != 0) {
+    fclose(f);
+    return 0;
+  }
+
+  char *data = malloc(st.st_size);
+  if (!data) {
+    fclose(f);
+    return 0;
+  }
+
+  if (fread(data, 1, st.st_size, f) != (size_t)st.st_size) {
+    fclose(f);
+    free(data);
+    return 0;
+  }
+
+  fclose(f);
+  return data;
+}
+
+static char *terminfo_try_path(const char *path, const char *term) {
+  char tmp[4096];
+  // snprintf guarantee for older compilers
+  assert(sizeof(tmp) > sizeof(path)+sizeof("/x/")+sizeof(term)+1);
+  snprintf(tmp, sizeof(tmp), "%s/%c/%s", path, term[0], term);
+  char *data = read_file(tmp);
+  if (data) {
+    return data;
+  }
+
+  // fallback to darwin specific dirs structure
+  // snprintf guarantee above still applies
+  snprintf(tmp, sizeof(tmp), "%s/%x/%s", path, term[0], term);
+  return read_file(tmp);
+}
+
+void string_copy(char* dest, const char* src, int dest_capacity) {
+  strncpy(dest, src, dest_capacity);
+  dest[dest_capacity-1] = '\0';
+}
+
+void string_append(char* dest, const char* src, int dest_capacity) {
+  strncat(dest, src, dest_capacity);
+  dest[dest_capacity-1] = '\0';
+}
+
+static char *load_terminfo(void) {
+  char tmp[4096];
+  const char *term = getenv("TERM");
+  if (!term) {
+    return 0;
+  }
+
+  // if TERMINFO is set, no other directory should be searched
+  const char *terminfo = getenv("TERMINFO");
+  if (terminfo) {
+    return terminfo_try_path(terminfo, term);
+  }
+
+  // next, consider ~/.terminfo
+  const char *home = getenv("HOME");
+  if (home) {
+    // snprintf guarantee for older compilers
+    assert(sizeof(tmp) > sizeof(home)+sizeof("/.terminfo")+1);
+    string_copy(tmp, home, sizeof(tmp));
+    string_append(tmp, "/.terminfo", sizeof(tmp));
+    char *data = terminfo_try_path(tmp, term);
+    if (data)
+      return data;
+  }
+
+  // next, TERMINFO_DIRS
+  const char *dirs = getenv("TERMINFO_DIRS");
+  if (dirs) {
+    // snprintf guarantee for older compilers
+    assert(sizeof(tmp) > sizeof(dirs));
+    strncpy(tmp, dirs, sizeof(tmp));
+    char *dir = strtok(tmp, ":");
+    while (dir) {
+      const char *cdir = dir;
+      if (strcmp(cdir, "") == 0) {
+        cdir = "/usr/share/terminfo";
+      }
+      char *data = terminfo_try_path(cdir, term);
+      if (data)
+        return data;
+      dir = strtok(0, ":");
+    }
+  }
+
+  // fallback to /usr/share/terminfo
+  return terminfo_try_path("/usr/share/terminfo", term);
+}
+
+#define TI_MAGIC 0432
+#define TI_HEADER_LENGTH 12
+#define TB_KEYS_NUM 22
+
+static const char *terminfo_copy_string(char *data, int str, int table) {
+  const int16_t off = *(int16_t*)(data + str);
+  const char *src = data + table + off;
+  int len = strlen(src);
+  char *dst = malloc(len+1);
+  string_copy(dst, src, len+1);
+  return dst;
+}
+
+static const int16_t ti_funcs[] = {
+  28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88,
+};
+
+static const int16_t ti_keys[] = {
+  66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69,
+  70, 71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61,
+  79, 83,
+};
+
+static int init_term(void) {
+  int i;
+  char *data = load_terminfo();
+  if (!data) {
+    init_from_terminfo = false;
+    return init_term_builtin();
+  }
+
+  int16_t *header = (int16_t*)data;
+  if ((header[1] + header[2]) % 2) {
+    // old quirk to align everything on word boundaries
+    header[2] += 1;
+  }
+
+  const int str_offset = TI_HEADER_LENGTH +
+    header[1] + header[2] + 2 * header[3];
+  const int table_offset = str_offset + 2 * header[4];
+
+  keys = malloc(sizeof(const char*) * (TB_KEYS_NUM+1));
+  for (i = 0; i < TB_KEYS_NUM; i++) {
+    keys[i] = terminfo_copy_string(data,
+      str_offset + 2 * ti_keys[i], table_offset);
+  }
+  keys[TB_KEYS_NUM] = 0;
+
+  funcs = malloc(sizeof(const char*) * T_FUNCS_NUM);
+  // the last four entries are reserved for mouse, bracketed paste. because the table offset is
+  // not there, the two entries have to fill in manually
+  for (i = 0; i < T_FUNCS_NUM-4; i++) {
+    funcs[i] = terminfo_copy_string(data,
+      str_offset + 2 * ti_funcs[i], table_offset);
+  }
+
+  funcs[T_FUNCS_NUM-4] = "\033[?1000h";
+  funcs[T_FUNCS_NUM-3] = "\033[?1000l";
+  funcs[T_FUNCS_NUM-2] = "\033[?2004h";
+  funcs[T_FUNCS_NUM-1] = "\033[?2004l";
+
+  init_from_terminfo = true;
+  free(data);
+  return 0;
+}
+
+static void shutdown_term(void) {
+  if (init_from_terminfo) {
+    int i;
+    for (i = 0; i < TB_KEYS_NUM; i++) {
+      free((void*)keys[i]);
+    }
+    // the last four entries are reserved for mouse, bracketed paste. because the table offset
+    // is not there, the two entries have to fill in manually and do not
+    // need to be freed.
+    for (i = 0; i < T_FUNCS_NUM-4; i++) {
+      free((void*)funcs[i]);
+    }
+    free(keys);
+    free(funcs);
+  }
+}
diff --git a/tools/termbox/termbox.c b/tools/termbox/termbox.c
new file mode 100644
index 00000000..c97f03d5
--- /dev/null
+++ b/tools/termbox/termbox.c
@@ -0,0 +1,397 @@
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdbool.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <sys/stat.h>
+#include <termios.h>
+#include <unistd.h>
+#include <wchar.h>
+/* hack: we can't define _XOPEN_SOURCE because that causes OpenBSD to not
+ * include SIGWINCH. But then this prototype is not included on Linux,
+ * triggering a warning. */
+extern int wcwidth (wchar_t);
+
+#include "termbox.h"
+
+#include "bytebuffer.inl"
+#include "output.inl"
+#include "input.inl"
+
+#define LAST_COORD_INIT -1
+
+static struct termios orig_tios;
+
+static struct bytebuffer output_buffer;
+static struct bytebuffer input_buffer;
+
+static int termw = -1;
+static int termh = -1;
+
+static int inout;
+static int winch_fds[2];
+
+static int cursor_x = 0;
+static int cursor_y = 0;
+
+static uint16_t background = TB_BLACK;
+static uint16_t foreground = TB_WHITE;
+
+static void update_size(void);
+static void update_term_size(void);
+static void send_attr(uint16_t fg, uint16_t bg);
+static void send_clear(void);
+static void sigwinch_handler(int xxx);
+static int wait_fill_event(struct tb_event *event, struct timeval *timeout);
+
+/* may happen in a different thread */
+static volatile int buffer_size_change_request;
+
+/* -------------------------------------------------------- */
+
+int tb_init(void)
+{
+  inout = open("/dev/tty", O_RDWR);
+  if (inout == -1) {
+    return TB_EFAILED_TO_OPEN_TTY;
+  }
+
+  if (init_term() < 0) {
+    close(inout);
+    return TB_EUNSUPPORTED_TERMINAL;
+  }
+
+  if (pipe(winch_fds) < 0) {
+    close(inout);
+    return TB_EPIPE_TRAP_ERROR;
+  }
+
+  struct sigaction sa;
+  memset(&sa, 0, sizeof(sa));
+  sa.sa_handler = sigwinch_handler;
+  sa.sa_flags = 0;
+  sigaction(SIGWINCH, &sa, 0);
+
+  tcgetattr(inout, &orig_tios);
+
+  struct termios tios;
+  memcpy(&tios, &orig_tios, sizeof(tios));
+
+  tios.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP
+                           | INLCR | IGNCR | ICRNL | IXON);
+  tios.c_oflag &= ~OPOST;
+  tios.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);
+  tios.c_cflag &= ~(CSIZE | PARENB);
+  tios.c_cflag |= CS8;
+  tios.c_cc[VMIN] = 0;
+  tios.c_cc[VTIME] = 0;
+  tcsetattr(inout, TCSAFLUSH, &tios);
+
+  bytebuffer_init(&input_buffer, 128);
+  bytebuffer_init(&output_buffer, 32 * 1024);
+
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_KEYPAD]);
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_MOUSE]);
+  bytebuffer_puts(&output_buffer, funcs[T_ENTER_BRACKETED_PASTE]);
+  bytebuffer_flush(&output_buffer, inout);
+
+  update_term_size();
+  return 0;
+}
+
+void tb_shutdown(void)
+{
+  if (termw == -1) return;
+
+  bytebuffer_puts(&output_buffer, funcs[T_SGR0]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_KEYPAD]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_MOUSE]);
+  bytebuffer_puts(&output_buffer, funcs[T_EXIT_BRACKETED_PASTE]);
+  bytebuffer_flush(&output_buffer, inout);
+  tcsetattr(inout, TCSAFLUSH, &orig_tios);
+
+  shutdown_term();
+  close(inout);
+  close(winch_fds[0]);
+  close(winch_fds[1]);
+
+  bytebuffer_free(&output_buffer);
+  bytebuffer_free(&input_buffer);
+  termw = termh = -1;
+}
+
+int tb_is_active(void)
+{
+  return termw != -1;
+}
+
+void tb_print(uint32_t ch, uint16_t fg, uint16_t bg)
+{
+  assert(termw != -1);
+  send_attr(fg, bg);
+  if (ch == 0) {
+    // replace 0 with whitespace
+    bytebuffer_puts(&output_buffer, " ");
+  }
+  else {
+    char buf[7];
+    int bw = tb_utf8_unicode_to_char(buf, ch);
+    buf[bw] = '\0';
+    bytebuffer_puts(&output_buffer, buf);
+  }
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+int tb_poll_event(struct tb_event *event)
+{
+  assert(termw != -1);
+  return wait_fill_event(event, 0);
+}
+
+int tb_peek_event(struct tb_event *event, int timeout)
+{
+  struct timeval tv;
+  tv.tv_sec = timeout / 1000;
+  tv.tv_usec = (timeout - (tv.tv_sec * 1000)) * 1000;
+  assert(termw != -1);
+  return wait_fill_event(event, &tv);
+}
+
+int tb_width(void)
+{
+  assert(termw != -1);
+  return termw;
+}
+
+int tb_height(void)
+{
+  assert(termw != -1);
+  return termh;
+}
+
+void tb_clear(void)
+{
+  assert(termw != -1);
+  if (buffer_size_change_request) {
+    update_size();
+    buffer_size_change_request = 0;
+  }
+  send_clear();
+}
+
+void tb_set_clear_attributes(uint16_t fg, uint16_t bg)
+{
+  assert(termw != -1);
+  foreground = fg;
+  background = bg;
+}
+
+/* -------------------------------------------------------- */
+
+static int convertnum(uint32_t num, char* buf) {
+  int i, l = 0;
+  int ch;
+  do {
+    buf[l++] = '0' + (num % 10);
+    num /= 10;
+  } while (num);
+  for(i = 0; i < l / 2; i++) {
+    ch = buf[i];
+    buf[i] = buf[l - 1 - i];
+    buf[l - 1 - i] = ch;
+  }
+  return l;
+}
+
+#define WRITE_LITERAL(X) bytebuffer_append(&output_buffer, (X), sizeof(X)-1)
+#define WRITE_INT(X) bytebuffer_append(&output_buffer, buf, convertnum((X), buf))
+
+void tb_set_cursor(int x, int y) {
+  char buf[32];
+  WRITE_LITERAL("\033[");
+  WRITE_INT(y+1);
+  WRITE_LITERAL(";");
+  WRITE_INT(x+1);
+  WRITE_LITERAL("H");
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+static void get_term_size(int *w, int *h)
+{
+  struct winsize sz;
+  memset(&sz, 0, sizeof(sz));
+
+  ioctl(inout, TIOCGWINSZ, &sz);
+
+  if (w) *w = sz.ws_col;
+  if (h) *h = sz.ws_row;
+}
+
+static void update_term_size(void)
+{
+  struct winsize sz;
+  memset(&sz, 0, sizeof(sz));
+
+  ioctl(inout, TIOCGWINSZ, &sz);
+
+  termw = sz.ws_col;
+  termh = sz.ws_row;
+}
+
+static void send_attr(uint16_t fg, uint16_t bg)
+{
+#define LAST_ATTR_INIT 0xFFFF
+  static uint16_t lastfg = LAST_ATTR_INIT, lastbg = LAST_ATTR_INIT;
+  if (fg != lastfg || bg != lastbg) {
+    bytebuffer_puts(&output_buffer, funcs[T_SGR0]);
+
+    uint16_t fgcol = fg & 0xFF;
+    uint16_t bgcol = bg & 0xFF;
+
+    if (fg & TB_BOLD)
+      bytebuffer_puts(&output_buffer, funcs[T_BOLD]);
+    if (bg & TB_BOLD)
+      bytebuffer_puts(&output_buffer, funcs[T_BLINK]);
+    if (fg & TB_UNDERLINE)
+      bytebuffer_puts(&output_buffer, funcs[T_UNDERLINE]);
+    if ((fg & TB_REVERSE) || (bg & TB_REVERSE))
+      bytebuffer_puts(&output_buffer, funcs[T_REVERSE]);
+    char buf[32];
+    WRITE_LITERAL("\033[38;5;");
+    WRITE_INT(fgcol);
+    WRITE_LITERAL("m");
+    WRITE_LITERAL("\033[48;5;");
+    WRITE_INT(bgcol);
+    WRITE_LITERAL("m");
+    bytebuffer_flush(&output_buffer, inout);
+    lastfg = fg;
+    lastbg = bg;
+  }
+}
+
+const char* to_unicode(uint32_t c)
+{
+  static char buf[7];
+  int bw = tb_utf8_unicode_to_char(buf, c);
+  buf[bw] = '\0';
+  return buf;
+}
+
+static void send_clear(void)
+{
+  send_attr(foreground, background);
+  bytebuffer_puts(&output_buffer, funcs[T_CLEAR_SCREEN]);
+  tb_set_cursor(cursor_x, cursor_y);
+  bytebuffer_flush(&output_buffer, inout);
+}
+
+static void sigwinch_handler(int xxx)
+{
+  (void) xxx;
+  const int zzz = 1;
+  int yyy = write(winch_fds[1], &zzz, sizeof(int));
+  (void) yyy;
+}
+
+static void update_size(void)
+{
+  update_term_size();
+  send_clear();
+}
+
+static int read_up_to(int n) {
+  assert(n > 0);
+  const int prevlen = input_buffer.len;
+  bytebuffer_resize(&input_buffer, prevlen + n);
+
+  int read_n = 0;
+  while (read_n <= n) {
+    ssize_t r = 0;
+    if (read_n < n) {
+      r = read(inout, input_buffer.buf + prevlen + read_n, n - read_n);
+    }
+#ifdef __CYGWIN__
+    // While linux man for tty says when VMIN == 0 && VTIME == 0, read
+    // should return 0 when there is nothing to read, cygwin's read returns
+    // -1. Not sure why and if it's correct to ignore it, but let's pretend
+    // it's zero.
+    if (r < 0) r = 0;
+#endif
+    if (r < 0) {
+      // EAGAIN / EWOULDBLOCK shouldn't occur here
+      assert(errno != EAGAIN && errno != EWOULDBLOCK);
+      return -1;
+    } else if (r > 0) {
+      read_n += r;
+    } else {
+      bytebuffer_resize(&input_buffer, prevlen + read_n);
+      return read_n;
+    }
+  }
+  assert(!"unreachable");
+  return 0;
+}
+
+int tb_event_ready(void)
+{
+  return input_buffer.len > 0;
+}
+
+static int wait_fill_event(struct tb_event *event, struct timeval *timeout)
+{
+  // ;-)
+#define ENOUGH_DATA_FOR_PARSING 64
+  fd_set events;
+  memset(event, 0, sizeof(struct tb_event));
+
+  // try to extract event from input buffer, return on success
+  event->type = TB_EVENT_KEY;
+  if (extract_event(event, &input_buffer))
+    return event->type;
+
+  // it looks like input buffer is incomplete, let's try the short path,
+  // but first make sure there is enough space
+  int n = read_up_to(ENOUGH_DATA_FOR_PARSING);
+  if (n < 0)
+    return -1;
+  if (n > 0 && extract_event(event, &input_buffer))
+    return event->type;
+
+  // n == 0, or not enough data, let's go to select
+  while (1) {
+    FD_ZERO(&events);
+    FD_SET(inout, &events);
+    FD_SET(winch_fds[0], &events);
+    int maxfd = (winch_fds[0] > inout) ? winch_fds[0] : inout;
+    int result = select(maxfd+1, &events, 0, 0, timeout);
+    if (!result)
+      return 0;
+
+    if (FD_ISSET(inout, &events)) {
+      event->type = TB_EVENT_KEY;
+      n = read_up_to(ENOUGH_DATA_FOR_PARSING);
+      if (n < 0)
+        return -1;
+
+      if (n == 0)
+        continue;
+
+      if (extract_event(event, &input_buffer))
+        return event->type;
+    }
+    if (FD_ISSET(winch_fds[0], &events)) {
+      event->type = TB_EVENT_RESIZE;
+      int zzz = 0;
+      int yyy = read(winch_fds[0], &zzz, sizeof(int));
+      (void) yyy;
+      buffer_size_change_request = 1;
+      get_term_size(&event->w, &event->h);
+      return TB_EVENT_RESIZE;
+    }
+  }
+}
diff --git a/tools/termbox/termbox.h b/tools/termbox/termbox.h
new file mode 100644
index 00000000..43b326cb
--- /dev/null
+++ b/tools/termbox/termbox.h
@@ -0,0 +1,190 @@
+#pragma once
+
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*** 1. Controlling the screen. */
+
+/* Names for some foreground/background colors. */
+#define TB_BLACK 232
+#define TB_WHITE 255
+
+/* Some attributes of screen cells that can be combined with colors using
+ * bitwise-OR. */
+#define TB_BOLD      0x0100
+#define TB_UNDERLINE 0x0200
+#define TB_REVERSE   0x0400
+
+/* Initialize screen and keyboard. */
+int tb_init(void);
+/* Possible error codes returned by tb_init() */
+#define TB_EUNSUPPORTED_TERMINAL -1
+#define TB_EFAILED_TO_OPEN_TTY   -2
+/* Termbox uses unix pipes in order to deliver a message from a signal handler
+ * (SIGWINCH) to the main event reading loop. */
+#define TB_EPIPE_TRAP_ERROR      -3
+
+/* Restore terminal mode. */
+void tb_shutdown(void);
+
+int tb_is_active(void);
+
+/* Size of the screen. Return negative values before tb_init() or after
+ * tb_shutdown() */
+int tb_width(void);
+int tb_height(void);
+
+/* Clear the screen using either TB_DEFAULT or the color/attributes set by
+ * tb_set_clear_attributes(). */
+void tb_clear(void);
+void tb_set_clear_attributes(uint16_t fg, uint16_t bg);
+
+/* Move the cursor. Upper-left character is (0, 0). */
+void tb_set_cursor(int cx, int cy);
+
+/* Modify the screen at the cursor. */
+void tb_print(uint32_t ch, uint16_t fg, uint16_t bg);
+
+/*** 2. Controlling keyboard events. */
+
+struct tb_event {
+  uint8_t type;
+  /* fields for type TB_EVENT_KEY. At most one of 'key' and 'ch' will be set at
+   * any time. */
+  uint16_t key;
+  uint32_t ch;
+  /* fields for type TB_EVENT_RESIZE */
+  int32_t w;
+  int32_t h;
+  /* fields for type TB_EVENT_MOUSE */
+  int32_t x;
+  int32_t y;
+};
+
+/* Possible values for tb_event.type. */
+#define TB_EVENT_KEY    1
+#define TB_EVENT_RESIZE 2
+#define TB_EVENT_MOUSE  3
+
+/* Possible values for tb_event.key. */
+#define TB_KEY_F1               (0xFFFF-0)
+#define TB_KEY_F2               (0xFFFF-1)
+#define TB_KEY_F3               (0xFFFF-2)
+#define TB_KEY_F4               (0xFFFF-3)
+#define TB_KEY_F5               (0xFFFF-4)
+#define TB_KEY_F6               (0xFFFF-5)
+#define TB_KEY_F7               (0xFFFF-6)
+#define TB_KEY_F8               (0xFFFF-7)
+#define TB_KEY_F9               (0xFFFF-8)
+#define TB_KEY_F10              (0xFFFF-9)
+#define TB_KEY_F11              (0xFFFF-10)
+#define TB_KEY_F12              (0xFFFF-11)
+#define TB_KEY_INSERT           (0xFFFF-12)
+#define TB_KEY_DELETE           (0xFFFF-13)
+#define TB_KEY_HOME             (0xFFFF-14)
+#define TB_KEY_END              (0xFFFF-15)
+#define TB_KEY_PGUP             (0xFFFF-16)
+#define TB_KEY_PGDN             (0xFFFF-17)
+#define TB_KEY_ARROW_UP         (0xFFFF-18)
+#define TB_KEY_ARROW_DOWN       (0xFFFF-19)
+#define TB_KEY_ARROW_LEFT       (0xFFFF-20)
+#define TB_KEY_ARROW_RIGHT      (0xFFFF-21)
+#define TB_KEY_MOUSE_LEFT       (0xFFFF-22)
+#define TB_KEY_MOUSE_RIGHT      (0xFFFF-23)
+#define TB_KEY_MOUSE_MIDDLE     (0xFFFF-24)
+#define TB_KEY_MOUSE_RELEASE    (0xFFFF-25)
+#define TB_KEY_MOUSE_WHEEL_UP   (0xFFFF-26)
+#define TB_KEY_MOUSE_WHEEL_DOWN (0xFFFF-27)
+#define TB_KEY_START_PASTE      (0xFFFF-28)
+#define TB_KEY_END_PASTE        (0xFFFF-29)
+#define TB_KEY_CTRL_ARROW_UP    (0xFFFF-30)
+#define TB_KEY_CTRL_ARROW_DOWN  (0xFFFF-31)
+#define TB_KEY_CTRL_ARROW_LEFT  (0xFFFF-32)
+#define TB_KEY_CTRL_ARROW_RIGHT (0xFFFF-33)
+#define TB_KEY_SHIFT_TAB        (0xFFFF-34)
+
+/* Names for some of the possible values for tb_event.ch. */
+/* These are all ASCII code points below SPACE character and a BACKSPACE key. */
+#define TB_KEY_CTRL_TILDE       0x00
+#define TB_KEY_CTRL_2           0x00 /* clash with 'CTRL_TILDE' */
+#define TB_KEY_CTRL_A           0x01
+#define TB_KEY_CTRL_B           0x02
+#define TB_KEY_CTRL_C           0x03
+#define TB_KEY_CTRL_D           0x04
+#define TB_KEY_CTRL_E           0x05
+#define TB_KEY_CTRL_F           0x06
+#define TB_KEY_CTRL_G           0x07
+#define TB_KEY_BACKSPACE        0x08
+#define TB_KEY_CTRL_H           0x08 /* clash with 'CTRL_BACKSPACE' */
+#define TB_KEY_TAB              0x09
+#define TB_KEY_CTRL_I           0x09 /* clash with 'TAB' */
+#define TB_KEY_CTRL_J           0x0A
+#define TB_KEY_CTRL_K           0x0B
+#define TB_KEY_CTRL_L           0x0C
+#define TB_KEY_ENTER            0x0D
+#define TB_KEY_CTRL_M           0x0D /* clash with 'ENTER' */
+#define TB_KEY_CTRL_N           0x0E
+#define TB_KEY_CTRL_O           0x0F
+#define TB_KEY_CTRL_P           0x10
+#define TB_KEY_CTRL_Q           0x11
+#define TB_KEY_CTRL_R           0x12
+#define TB_KEY_CTRL_S           0x13
+#define TB_KEY_CTRL_T           0x14
+#define TB_KEY_CTRL_U           0x15
+#define TB_KEY_CTRL_V           0x16
+#define TB_KEY_CTRL_W           0x17
+#define TB_KEY_CTRL_X           0x18
+#define TB_KEY_CTRL_Y           0x19
+#define TB_KEY_CTRL_Z           0x1A
+#define TB_KEY_ESC              0x1B
+#define TB_KEY_CTRL_LSQ_BRACKET 0x1B /* clash with 'ESC' */
+#define TB_KEY_CTRL_3           0x1B /* clash with 'ESC' */
+#define TB_KEY_CTRL_4           0x1C
+#define TB_KEY_CTRL_BACKSLASH   0x1C /* clash with 'CTRL_4' */
+#define TB_KEY_CTRL_5           0x1D
+#define TB_KEY_CTRL_RSQ_BRACKET 0x1D /* clash with 'CTRL_5' */
+#define TB_KEY_CTRL_6           0x1E
+#define TB_KEY_CTRL_7           0x1F
+#define TB_KEY_CTRL_SLASH       0x1F /* clash with 'CTRL_7' */
+#define TB_KEY_CTRL_UNDERSCORE  0x1F /* clash with 'CTRL_7' */
+#define TB_KEY_SPACE            0x20
+#define TB_KEY_BACKSPACE2       0x7F
+#define TB_KEY_CTRL_8           0x7F /* clash with 'DELETE' */
+/* These are non-existing ones.
+ *
+ * #define TB_KEY_CTRL_1 clash with '1'
+ * #define TB_KEY_CTRL_9 clash with '9'
+ * #define TB_KEY_CTRL_0 clash with '0'
+ */
+/* Some aliases */
+#define TB_KEY_NEWLINE TB_KEY_CTRL_J
+#define TB_KEY_CARRIAGE_RETURN TB_KEY_CTRL_M
+
+/* Wait for an event up to 'timeout' milliseconds and fill the 'event'
+ * structure with it, when the event is available. Returns the type of the
+ * event (one of TB_EVENT_* constants) or -1 if there was an error or 0 in case
+ * there were no event during 'timeout' period.
+ */
+int tb_peek_event(struct tb_event *event, int timeout);
+
+/* Wait for an event forever and fill the 'event' structure with it, when the
+ * event is available. Returns the type of the event (one of TB_EVENT_*
+ * constants) or -1 if there was an error.
+ */
+int tb_poll_event(struct tb_event *event);
+
+int tb_event_ready(void);
+
+/*** 3. Utility utf8 functions. */
+#define TB_EOF -1
+int tb_utf8_char_length(char c);
+int tb_utf8_char_to_unicode(uint32_t *out, const char *c);
+int tb_utf8_unicode_to_char(char *out, uint32_t c);
+const char* to_unicode(uint32_t c);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/tools/termbox/utf8.c b/tools/termbox/utf8.c
new file mode 100644
index 00000000..26c0c27b
--- /dev/null
+++ b/tools/termbox/utf8.c
@@ -0,0 +1,79 @@
+#include "termbox.h"
+
+static const unsigned char utf8_length[256] = {
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
+  2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+  3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,4,4,4,4,4,4,4,4,5,5,5,5,6,6,1,1
+};
+
+static const unsigned char utf8_mask[6] = {
+  0x7F,
+  0x1F,
+  0x0F,
+  0x07,
+  0x03,
+  0x01
+};
+
+int tb_utf8_char_length(char c)
+{
+  return utf8_length[(unsigned char)c];
+}
+
+int tb_utf8_char_to_unicode(uint32_t *out, const char *c)
+{
+  if (*c == 0)
+    return TB_EOF;
+
+  int i;
+  unsigned char len = tb_utf8_char_length(*c);
+  unsigned char mask = utf8_mask[len-1];
+  uint32_t result = c[0] & mask;
+  for (i = 1; i < len; ++i) {
+    result <<= 6;
+    result |= c[i] & 0x3f;
+  }
+
+  *out = result;
+  return (int)len;
+}
+
+int tb_utf8_unicode_to_char(char *out, uint32_t c)
+{
+  int len = 0;
+  int first;
+  int i;
+
+  if (c < 0x80) {
+    first = 0;
+    len = 1;
+  } else if (c < 0x800) {
+    first = 0xc0;
+    len = 2;
+  } else if (c < 0x10000) {
+    first = 0xe0;
+    len = 3;
+  } else if (c < 0x200000) {
+    first = 0xf0;
+    len = 4;
+  } else if (c < 0x4000000) {
+    first = 0xf8;
+    len = 5;
+  } else {
+    first = 0xfc;
+    len = 6;
+  }
+
+  for (i = len - 1; i > 0; --i) {
+    out[i] = (c & 0x3f) | 0x80;
+    c >>= 6;
+  }
+  out[0] = c | first;
+
+  return len;
+}