Update to upstream revision 727

This is the newest version with changes to how the color handling is
done.

Test: start Terminal.apk with and without USE_TEST_SHELL
Change-Id: I14decec8617ab108f2236fc66890bb1e7ab17394
diff --git a/Android.bp b/Android.bp
index 33d3b75..2a97539 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4,14 +4,15 @@
     export_include_dirs: ["include"],
 
     srcs: [
-        "src/input.c",
-        "src/vterm.c",
         "src/encoding.c",
+        "src/input.c",
+        "src/keyboard.c",
         "src/parser.c",
-        "src/unicode.c",
         "src/pen.c",
         "src/screen.c",
         "src/state.c",
+        "src/unicode.c",
+        "src/vterm.c",
     ],
 
     cflags: [
diff --git a/include/vterm.h b/include/vterm.h
index d43add7..51dc970 100644
--- a/include/vterm.h
+++ b/include/vterm.h
@@ -7,8 +7,9 @@
 
 #include <stdint.h>
 #include <stdlib.h>
+#include <stdbool.h>
 
-#include "vterm_input.h"
+#include "vterm_keycodes.h"
 
 typedef struct VTerm VTerm;
 typedef struct VTermState VTermState;
@@ -48,16 +49,160 @@
   rect->start_col += col_delta; rect->end_col += col_delta;
 }
 
-typedef struct {
-  uint8_t red, green, blue;
+/**
+ * Bit-field describing the content of the tagged union `VTermColor`.
+ */
+typedef enum {
+  /**
+   * If the lower bit of `type` is not set, the colour is 24-bit RGB.
+   */
+  VTERM_COLOR_RGB = 0x00,
+
+  /**
+   * The colour is an index into a palette of 256 colours.
+   */
+  VTERM_COLOR_INDEXED = 0x01,
+
+  /**
+   * Mask that can be used to extract the RGB/Indexed bit.
+   */
+  VTERM_COLOR_TYPE_MASK = 0x01,
+
+  /**
+   * If set, indicates that this colour should be the default foreground
+   * color, i.e. there was no SGR request for another colour. When
+   * rendering this colour it is possible to ignore "idx" and just use a
+   * colour that is not in the palette.
+   */
+  VTERM_COLOR_DEFAULT_FG = 0x02,
+
+  /**
+   * If set, indicates that this colour should be the default background
+   * color, i.e. there was no SGR request for another colour. A common
+   * option when rendering this colour is to not render a background at
+   * all, for example by rendering the window transparently at this spot.
+   */
+  VTERM_COLOR_DEFAULT_BG = 0x04,
+
+  /**
+   * Mask that can be used to extract the default foreground/background bit.
+   */
+  VTERM_COLOR_DEFAULT_MASK = 0x06
+} VTermColorType;
+
+/**
+ * Returns true if the VTERM_COLOR_RGB `type` flag is set, indicating that the
+ * given VTermColor instance is an indexed colour.
+ */
+#define VTERM_COLOR_IS_INDEXED(col) \
+  (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_INDEXED)
+
+/**
+ * Returns true if the VTERM_COLOR_INDEXED `type` flag is set, indicating that
+ * the given VTermColor instance is an rgb colour.
+ */
+#define VTERM_COLOR_IS_RGB(col) \
+  (((col)->type & VTERM_COLOR_TYPE_MASK) == VTERM_COLOR_RGB)
+
+/**
+ * Returns true if the VTERM_COLOR_DEFAULT_FG `type` flag is set, indicating
+ * that the given VTermColor instance corresponds to the default foreground
+ * color.
+ */
+#define VTERM_COLOR_IS_DEFAULT_FG(col) \
+  (!!((col)->type & VTERM_COLOR_DEFAULT_FG))
+
+/**
+ * Returns true if the VTERM_COLOR_DEFAULT_BG `type` flag is set, indicating
+ * that the given VTermColor instance corresponds to the default background
+ * color.
+ */
+#define VTERM_COLOR_IS_DEFAULT_BG(col) \
+  (!!((col)->type & VTERM_COLOR_DEFAULT_BG))
+
+/**
+ * Tagged union storing either an RGB color or an index into a colour palette.
+ * In order to convert indexed colours to RGB, you may use the
+ * vterm_state_convert_color_to_rgb() or vterm_screen_convert_color_to_rgb()
+ * functions which lookup the RGB colour from the palette maintained by a
+ * VTermState or VTermScreen instance.
+ */
+typedef union {
+  /**
+   * Tag indicating which union member is actually valid. This variable
+   * coincides with the `type` member of the `rgb` and the `indexed` struct
+   * in memory. Please use the `VTERM_COLOR_IS_*` test macros to check whether
+   * a particular type flag is set.
+   */
+  uint8_t type;
+
+  /**
+   * Valid if `VTERM_COLOR_IS_RGB(type)` is true. Holds the RGB colour values.
+   */
+  struct {
+    /**
+     * Same as the top-level `type` member stored in VTermColor.
+     */
+    uint8_t type;
+
+    /**
+     * The actual 8-bit red, green, blue colour values.
+     */
+    uint8_t red, green, blue;
+  } rgb;
+
+  /**
+   * If `VTERM_COLOR_IS_INDEXED(type)` is true, this member holds the index into
+   * the colour palette.
+   */
+  struct {
+    /**
+     * Same as the top-level `type` member stored in VTermColor.
+     */
+    uint8_t type;
+
+    /**
+     * Index into the colour map.
+     */
+    uint8_t idx;
+  } indexed;
 } VTermColor;
 
+/**
+ * Constructs a new VTermColor instance representing the given RGB values.
+ */
+static inline void vterm_color_rgb(VTermColor *col, uint8_t red, uint8_t green,
+                                   uint8_t blue)
+{
+  col->type = VTERM_COLOR_RGB;
+  col->rgb.red   = red;
+  col->rgb.green = green;
+  col->rgb.blue  = blue;
+}
+
+/**
+ * Construct a new VTermColor instance representing an indexed color with the
+ * given index.
+ */
+static inline void vterm_color_indexed(VTermColor *col, uint8_t idx)
+{
+  col->type = VTERM_COLOR_INDEXED;
+  col->indexed.idx = idx;
+}
+
+/**
+ * Compares two colours. Returns true if the colors are equal, false otherwise.
+ */
+int vterm_color_is_equal(const VTermColor *a, const VTermColor *b);
+
 typedef enum {
   /* VTERM_VALUETYPE_NONE = 0 */
   VTERM_VALUETYPE_BOOL = 1,
   VTERM_VALUETYPE_INT,
   VTERM_VALUETYPE_STRING,
   VTERM_VALUETYPE_COLOR,
+
+  VTERM_N_VALUETYPES
 } VTermValueType;
 
 typedef union {
@@ -78,6 +223,8 @@
   VTERM_ATTR_FONT,       // number: 10-19
   VTERM_ATTR_FOREGROUND, // color:  30-39 90-97
   VTERM_ATTR_BACKGROUND, // color:  40-49 100-107
+
+  VTERM_N_ATTRS
 } VTermAttr;
 
 typedef enum {
@@ -89,15 +236,27 @@
   VTERM_PROP_ICONNAME,          // string
   VTERM_PROP_REVERSE,           // bool
   VTERM_PROP_CURSORSHAPE,       // number
+  VTERM_PROP_MOUSE,             // number
+
+  VTERM_N_PROPS
 } VTermProp;
 
 enum {
   VTERM_PROP_CURSORSHAPE_BLOCK = 1,
   VTERM_PROP_CURSORSHAPE_UNDERLINE,
   VTERM_PROP_CURSORSHAPE_BAR_LEFT,
+
+  VTERM_N_PROP_CURSORSHAPES
 };
 
-typedef void (*VTermMouseFunc)(int x, int y, int button, int pressed, int modifiers, void *data);
+enum {
+  VTERM_PROP_MOUSE_NONE = 0,
+  VTERM_PROP_MOUSE_CLICK,
+  VTERM_PROP_MOUSE_DRAG,
+  VTERM_PROP_MOUSE_MOVE,
+
+  VTERM_N_PROP_MOUSES
+};
 
 typedef struct {
   const uint32_t *chars;
@@ -126,18 +285,25 @@
 void vterm_get_size(const VTerm *vt, int *rowsp, int *colsp);
 void vterm_set_size(VTerm *vt, int rows, int cols);
 
-void vterm_push_bytes(VTerm *vt, const char *bytes, size_t len);
+int  vterm_get_utf8(const VTerm *vt);
+void vterm_set_utf8(VTerm *vt, int is_utf8);
 
-void vterm_input_push_char(VTerm *vt, VTermModifier state, uint32_t c);
-void vterm_input_push_key(VTerm *vt, VTermModifier state, VTermKey key);
-
-size_t vterm_output_bufferlen(VTerm *vt); /* deprecated */
+size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len);
 
 size_t vterm_output_get_buffer_size(const VTerm *vt);
 size_t vterm_output_get_buffer_current(const VTerm *vt);
 size_t vterm_output_get_buffer_remaining(const VTerm *vt);
 
-size_t vterm_output_bufferread(VTerm *vt, char *buffer, size_t len);
+size_t vterm_output_read(VTerm *vt, char *buffer, size_t len);
+
+void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod);
+void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod);
+
+void vterm_keyboard_start_paste(VTerm *vt);
+void vterm_keyboard_end_paste(VTerm *vt);
+
+void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod);
+void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod);
 
 // ------------
 // Parser layer
@@ -151,8 +317,8 @@
  *
  * Don't confuse this with the final byte of the CSI escape; 'a' in this case.
  */
-#define CSI_ARG_FLAG_MORE (1<<31)
-#define CSI_ARG_MASK      (~(1<<31))
+#define CSI_ARG_FLAG_MORE (1U<<31)
+#define CSI_ARG_MASK      (~(1U<<31))
 
 #define CSI_ARG_HAS_MORE(a) ((a) & CSI_ARG_FLAG_MORE)
 #define CSI_ARG(a)          ((a) & CSI_ARG_MASK)
@@ -174,9 +340,8 @@
   int (*resize)(int rows, int cols, void *user);
 } VTermParserCallbacks;
 
-void vterm_set_parser_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user);
-
-void vterm_parser_set_utf8(VTerm *vt, int is_utf8);
+void  vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user);
+void *vterm_parser_get_cbdata(VTerm *vt);
 
 // -----------
 // State layer
@@ -191,7 +356,6 @@
   int (*initpen)(void *user);
   int (*setpenattr)(VTermAttr attr, VTermValue *val, void *user);
   int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
-  int (*setmousefunc)(VTermMouseFunc func, void *data, void *user);
   int (*bell)(void *user);
   int (*resize)(int rows, int cols, VTermPos *delta, void *user);
   int (*setlineinfo)(int row, const VTermLineInfo *newinfo, const VTermLineInfo *oldinfo, void *user);
@@ -199,8 +363,14 @@
 
 VTermState *vterm_obtain_state(VTerm *vt);
 
+void  vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user);
+void *vterm_state_get_cbdata(VTermState *state);
+
+// Only invokes control, csi, osc, dcs
+void  vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermParserCallbacks *fallbacks, void *user);
+void *vterm_state_get_unrecognised_fbdata(VTermState *state);
+
 void vterm_state_reset(VTermState *state, int hard);
-void vterm_state_set_callbacks(VTermState *state, const VTermStateCallbacks *callbacks, void *user);
 void vterm_state_get_cursorpos(const VTermState *state, VTermPos *cursorpos);
 void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg);
 void vterm_state_get_palette_color(const VTermState *state, int index, VTermColor *col);
@@ -209,17 +379,27 @@
 void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright);
 int  vterm_state_get_penattr(const VTermState *state, VTermAttr attr, VTermValue *val);
 int  vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val);
+void vterm_state_focus_in(VTermState *state);
+void vterm_state_focus_out(VTermState *state);
 const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row);
 
+/**
+ * Makes sure that the given color `col` is indeed an RGB colour. After this
+ * function returns, VTERM_COLOR_IS_RGB(col) will return true, while all other
+ * flags stored in `col->type` will have been reset.
+ *
+ * @param state is the VTermState instance from which the colour palette should
+ * be extracted.
+ * @param col is a pointer at the VTermColor instance that should be converted
+ * to an RGB colour.
+ */
+void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col);
+
 // ------------
 // Screen layer
 // ------------
 
 typedef struct {
-#define VTERM_MAX_CHARS_PER_CELL 6
-  uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
-  char     width;
-  struct {
     unsigned int bold      : 1;
     unsigned int underline : 2;
     unsigned int italic    : 1;
@@ -229,7 +409,13 @@
     unsigned int font      : 4; /* 0 to 9 */
     unsigned int dwl       : 1; /* On a DECDWL or DECDHL line */
     unsigned int dhl       : 2; /* On a DECDHL line (1=top 2=bottom) */
-  } attrs;
+} VTermScreenCellAttrs;
+
+typedef struct {
+#define VTERM_MAX_CHARS_PER_CELL 6
+  uint32_t chars[VTERM_MAX_CHARS_PER_CELL];
+  char     width;
+  VTermScreenCellAttrs attrs;
   VTermColor fg, bg;
 } VTermScreenCell;
 
@@ -238,7 +424,6 @@
   int (*moverect)(VTermRect dest, VTermRect src, void *user);
   int (*movecursor)(VTermPos pos, VTermPos oldpos, int visible, void *user);
   int (*settermprop)(VTermProp prop, VTermValue *val, void *user);
-  int (*setmousefunc)(VTermMouseFunc func, void *data, void *user);
   int (*bell)(void *user);
   int (*resize)(int rows, int cols, void *user);
   int (*sb_pushline)(int cols, const VTermScreenCell *cells, void *user);
@@ -247,14 +432,22 @@
 
 VTermScreen *vterm_obtain_screen(VTerm *vt);
 
+void  vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user);
+void *vterm_screen_get_cbdata(VTermScreen *screen);
+
+// Only invokes control, csi, osc, dcs
+void  vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermParserCallbacks *fallbacks, void *user);
+void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen);
+
 void vterm_screen_enable_altscreen(VTermScreen *screen, int altscreen);
-void vterm_screen_set_callbacks(VTermScreen *screen, const VTermScreenCallbacks *callbacks, void *user);
 
 typedef enum {
   VTERM_DAMAGE_CELL,    /* every cell */
   VTERM_DAMAGE_ROW,     /* entire rows */
   VTERM_DAMAGE_SCREEN,  /* entire screen */
   VTERM_DAMAGE_SCROLL,  /* entire screen + scrollrect */
+
+  VTERM_N_DAMAGES
 } VTermDamageSize;
 
 void vterm_screen_flush_damage(VTermScreen *screen);
@@ -276,6 +469,8 @@
   VTERM_ATTR_FONT_MASK       = 1 << 6,
   VTERM_ATTR_FOREGROUND_MASK = 1 << 7,
   VTERM_ATTR_BACKGROUND_MASK = 1 << 8,
+
+  VTERM_ALL_ATTRS_MASK = (1 << 9) - 1
 } VTermAttrMask;
 
 int vterm_screen_get_attrs_extent(const VTermScreen *screen, VTermRect *extent, VTermPos pos, VTermAttrMask attrs);
@@ -284,6 +479,12 @@
 
 int vterm_screen_is_eol(const VTermScreen *screen, VTermPos pos);
 
+/**
+ * Same as vterm_state_convert_color_to_rgb(), but takes a `screen` instead of a `state`
+ * instance.
+ */
+void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col);
+
 // ---------
 // Utilities
 // ---------
diff --git a/include/vterm_input.h b/include/vterm_keycodes.h
similarity index 94%
rename from include/vterm_input.h
rename to include/vterm_keycodes.h
index 165d747..661759f 100644
--- a/include/vterm_input.h
+++ b/include/vterm_keycodes.h
@@ -6,6 +6,8 @@
   VTERM_MOD_SHIFT = 0x01,
   VTERM_MOD_ALT   = 0x02,
   VTERM_MOD_CTRL  = 0x04,
+
+  VTERM_ALL_MODS_MASK = 0x07 
 } VTermModifier;
 
 typedef enum {
@@ -51,6 +53,7 @@
   VTERM_KEY_KP_EQUAL,
 
   VTERM_KEY_MAX, // Must be last
+  VTERM_N_KEYS = VTERM_KEY_MAX
 } VTermKey;
 
 #define VTERM_KEY_FUNCTION(n) (VTERM_KEY_FUNCTION_0+(n))
diff --git a/src/encoding.c b/src/encoding.c
index 1495855..434ac3f 100644
--- a/src/encoding.c
+++ b/src/encoding.c
@@ -42,10 +42,10 @@
     printf(" pos=%zd c=%02x rem=%d\n", *pos, c, data->bytes_remaining);
 #endif
 
-    if(c < 0x20)
+    if(c < 0x20) // C0
       return;
 
-    else if(c >= 0x20 && c < 0x80) {
+    else if(c >= 0x20 && c < 0x7f) {
       if(data->bytes_remaining)
         cp[(*cpi)++] = UNICODE_INVALID;
 
@@ -56,6 +56,9 @@
       data->bytes_remaining = 0;
     }
 
+    else if(c == 0x7f) // DEL
+      return;
+
     else if(c >= 0x80 && c < 0xc0) {
       if(!data->bytes_remaining) {
         cp[(*cpi)++] = UNICODE_INVALID;
@@ -73,15 +76,20 @@
         // Check for overlong sequences
         switch(data->bytes_total) {
         case 2:
-          if(data->this_cp <  0x0080) data->this_cp = UNICODE_INVALID; break;
+          if(data->this_cp <  0x0080) data->this_cp = UNICODE_INVALID;
+          break;
         case 3:
-          if(data->this_cp <  0x0800) data->this_cp = UNICODE_INVALID; break;
+          if(data->this_cp <  0x0800) data->this_cp = UNICODE_INVALID;
+          break;
         case 4:
-          if(data->this_cp < 0x10000) data->this_cp = UNICODE_INVALID; break;
+          if(data->this_cp < 0x10000) data->this_cp = UNICODE_INVALID;
+          break;
         case 5:
-          if(data->this_cp < 0x200000) data->this_cp = UNICODE_INVALID; break;
+          if(data->this_cp < 0x200000) data->this_cp = UNICODE_INVALID;
+          break;
         case 6:
-          if(data->this_cp < 0x4000000) data->this_cp = UNICODE_INVALID; break;
+          if(data->this_cp < 0x4000000) data->this_cp = UNICODE_INVALID;
+          break;
         }
         // Now look for plain invalid ones
         if((data->this_cp >= 0xD800 && data->this_cp <= 0xDFFF) ||
@@ -160,7 +168,7 @@
   for(; *pos < bytelen && *cpi < cplen; (*pos)++) {
     unsigned char c = bytes[*pos] ^ is_gr;
 
-    if(c < 0x20 || c >= 0x80)
+    if(c < 0x20 || c == 0x7f || c >= 0x80)
       return;
 
     cp[(*cpi)++] = c;
@@ -186,7 +194,7 @@
   for(; *pos < bytelen && *cpi < cplen; (*pos)++) {
     unsigned char c = bytes[*pos] ^ is_gr;
 
-    if(c < 0x20 || c >= 0x80)
+    if(c < 0x20 || c == 0x7f || c >= 0x80)
       return;
 
     if(table->chars[c])
diff --git a/src/fullwidth.inc b/src/fullwidth.inc
new file mode 100644
index 0000000..7ff142f
--- /dev/null
+++ b/src/fullwidth.inc
@@ -0,0 +1,104 @@
+  { 0x1100, 0x115f },
+  { 0x231a, 0x231b },
+  { 0x2329, 0x232a },
+  { 0x23e9, 0x23ec },
+  { 0x23f0, 0x23f0 },
+  { 0x23f3, 0x23f3 },
+  { 0x25fd, 0x25fe },
+  { 0x2614, 0x2615 },
+  { 0x2648, 0x2653 },
+  { 0x267f, 0x267f },
+  { 0x2693, 0x2693 },
+  { 0x26a1, 0x26a1 },
+  { 0x26aa, 0x26ab },
+  { 0x26bd, 0x26be },
+  { 0x26c4, 0x26c5 },
+  { 0x26ce, 0x26ce },
+  { 0x26d4, 0x26d4 },
+  { 0x26ea, 0x26ea },
+  { 0x26f2, 0x26f3 },
+  { 0x26f5, 0x26f5 },
+  { 0x26fa, 0x26fa },
+  { 0x26fd, 0x26fd },
+  { 0x2705, 0x2705 },
+  { 0x270a, 0x270b },
+  { 0x2728, 0x2728 },
+  { 0x274c, 0x274c },
+  { 0x274e, 0x274e },
+  { 0x2753, 0x2755 },
+  { 0x2757, 0x2757 },
+  { 0x2795, 0x2797 },
+  { 0x27b0, 0x27b0 },
+  { 0x27bf, 0x27bf },
+  { 0x2b1b, 0x2b1c },
+  { 0x2b50, 0x2b50 },
+  { 0x2b55, 0x2b55 },
+  { 0x2e80, 0x2e99 },
+  { 0x2e9b, 0x2ef3 },
+  { 0x2f00, 0x2fd5 },
+  { 0x2ff0, 0x2ffb },
+  { 0x3000, 0x303e },
+  { 0x3041, 0x3096 },
+  { 0x3099, 0x30ff },
+  { 0x3105, 0x312d },
+  { 0x3131, 0x318e },
+  { 0x3190, 0x31ba },
+  { 0x31c0, 0x31e3 },
+  { 0x31f0, 0x321e },
+  { 0x3220, 0x3247 },
+  { 0x3250, 0x32fe },
+  { 0x3300, 0x4dbf },
+  { 0x4e00, 0xa48c },
+  { 0xa490, 0xa4c6 },
+  { 0xa960, 0xa97c },
+  { 0xac00, 0xd7a3 },
+  { 0xf900, 0xfaff },
+  { 0xfe10, 0xfe19 },
+  { 0xfe30, 0xfe52 },
+  { 0xfe54, 0xfe66 },
+  { 0xfe68, 0xfe6b },
+  { 0xff01, 0xff60 },
+  { 0xffe0, 0xffe6 },
+  { 0x16fe0, 0x16fe0 },
+  { 0x17000, 0x187ec },
+  { 0x18800, 0x18af2 },
+  { 0x1b000, 0x1b001 },
+  { 0x1f004, 0x1f004 },
+  { 0x1f0cf, 0x1f0cf },
+  { 0x1f18e, 0x1f18e },
+  { 0x1f191, 0x1f19a },
+  { 0x1f200, 0x1f202 },
+  { 0x1f210, 0x1f23b },
+  { 0x1f240, 0x1f248 },
+  { 0x1f250, 0x1f251 },
+  { 0x1f300, 0x1f320 },
+  { 0x1f32d, 0x1f335 },
+  { 0x1f337, 0x1f37c },
+  { 0x1f37e, 0x1f393 },
+  { 0x1f3a0, 0x1f3ca },
+  { 0x1f3cf, 0x1f3d3 },
+  { 0x1f3e0, 0x1f3f0 },
+  { 0x1f3f4, 0x1f3f4 },
+  { 0x1f3f8, 0x1f43e },
+  { 0x1f440, 0x1f440 },
+  { 0x1f442, 0x1f4fc },
+  { 0x1f4ff, 0x1f53d },
+  { 0x1f54b, 0x1f54e },
+  { 0x1f550, 0x1f567 },
+  { 0x1f57a, 0x1f57a },
+  { 0x1f595, 0x1f596 },
+  { 0x1f5a4, 0x1f5a4 },
+  { 0x1f5fb, 0x1f64f },
+  { 0x1f680, 0x1f6c5 },
+  { 0x1f6cc, 0x1f6cc },
+  { 0x1f6d0, 0x1f6d2 },
+  { 0x1f6eb, 0x1f6ec },
+  { 0x1f6f4, 0x1f6f6 },
+  { 0x1f910, 0x1f91e },
+  { 0x1f920, 0x1f927 },
+  { 0x1f930, 0x1f930 },
+  { 0x1f933, 0x1f93e },
+  { 0x1f940, 0x1f94b },
+  { 0x1f950, 0x1f95e },
+  { 0x1f980, 0x1f991 },
+  { 0x1f9c0, 0x1f9c0 },
diff --git a/src/keyboard.c b/src/keyboard.c
new file mode 100644
index 0000000..b541fb1
--- /dev/null
+++ b/src/keyboard.c
@@ -0,0 +1,226 @@
+#include "vterm_internal.h"
+
+#include <stdio.h>
+
+#include "utf8.h"
+
+void vterm_keyboard_unichar(VTerm *vt, uint32_t c, VTermModifier mod)
+{
+  /* The shift modifier is never important for Unicode characters
+   * apart from Space
+   */
+  if(c != ' ')
+    mod &= ~VTERM_MOD_SHIFT;
+
+  if(mod == 0) {
+    // Normal text - ignore just shift
+    char str[6];
+    int seqlen = fill_utf8(c, str);
+    vterm_push_output_bytes(vt, str, seqlen);
+    return;
+  }
+
+  int needs_CSIu;
+  switch(c) {
+    /* Special Ctrl- letters that can't be represented elsewise */
+    case 'i': case 'j': case 'm': case '[':
+      needs_CSIu = 1;
+      break;
+    /* Ctrl-\ ] ^ _ don't need CSUu */
+    case '\\': case ']': case '^': case '_':
+      needs_CSIu = 0;
+      break;
+    /* Shift-space needs CSIu */
+    case ' ':
+      needs_CSIu = !!(mod & VTERM_MOD_SHIFT);
+      break;
+    /* All other characters needs CSIu except for letters a-z */
+    default:
+      needs_CSIu = (c < 'a' || c > 'z');
+  }
+
+  /* ALT we can just prefix with ESC; anything else requires CSI u */
+  if(needs_CSIu && (mod & ~VTERM_MOD_ALT)) {
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", c, mod+1);
+    return;
+  }
+
+  if(mod & VTERM_MOD_CTRL)
+    c &= 0x1f;
+
+  vterm_push_output_sprintf(vt, "%s%c", mod & VTERM_MOD_ALT ? ESC_S : "", c);
+}
+
+typedef struct {
+  enum {
+    KEYCODE_NONE,
+    KEYCODE_LITERAL,
+    KEYCODE_TAB,
+    KEYCODE_ENTER,
+    KEYCODE_SS3,
+    KEYCODE_CSI,
+    KEYCODE_CSI_CURSOR,
+    KEYCODE_CSINUM,
+    KEYCODE_KEYPAD,
+  } type;
+  char literal;
+  int csinum;
+} keycodes_s;
+
+static keycodes_s keycodes[] = {
+  { KEYCODE_NONE }, // NONE
+
+  { KEYCODE_ENTER,   '\r'   }, // ENTER
+  { KEYCODE_TAB,     '\t'   }, // TAB
+  { KEYCODE_LITERAL, '\x7f' }, // BACKSPACE == ASCII DEL
+  { KEYCODE_LITERAL, '\x1b' }, // ESCAPE
+
+  { KEYCODE_CSI_CURSOR, 'A' }, // UP
+  { KEYCODE_CSI_CURSOR, 'B' }, // DOWN
+  { KEYCODE_CSI_CURSOR, 'D' }, // LEFT
+  { KEYCODE_CSI_CURSOR, 'C' }, // RIGHT
+
+  { KEYCODE_CSINUM, '~', 2 },  // INS
+  { KEYCODE_CSINUM, '~', 3 },  // DEL
+  { KEYCODE_CSI_CURSOR, 'H' }, // HOME
+  { KEYCODE_CSI_CURSOR, 'F' }, // END
+  { KEYCODE_CSINUM, '~', 5 },  // PAGEUP
+  { KEYCODE_CSINUM, '~', 6 },  // PAGEDOWN
+};
+
+static keycodes_s keycodes_fn[] = {
+  { KEYCODE_NONE },            // F0 - shouldn't happen
+  { KEYCODE_CSI_CURSOR, 'P' }, // F1
+  { KEYCODE_CSI_CURSOR, 'Q' }, // F2
+  { KEYCODE_CSI_CURSOR, 'R' }, // F3
+  { KEYCODE_CSI_CURSOR, 'S' }, // F4
+  { KEYCODE_CSINUM, '~', 15 }, // F5
+  { KEYCODE_CSINUM, '~', 17 }, // F6
+  { KEYCODE_CSINUM, '~', 18 }, // F7
+  { KEYCODE_CSINUM, '~', 19 }, // F8
+  { KEYCODE_CSINUM, '~', 20 }, // F9
+  { KEYCODE_CSINUM, '~', 21 }, // F10
+  { KEYCODE_CSINUM, '~', 23 }, // F11
+  { KEYCODE_CSINUM, '~', 24 }, // F12
+};
+
+static keycodes_s keycodes_kp[] = {
+  { KEYCODE_KEYPAD, '0', 'p' }, // KP_0
+  { KEYCODE_KEYPAD, '1', 'q' }, // KP_1
+  { KEYCODE_KEYPAD, '2', 'r' }, // KP_2
+  { KEYCODE_KEYPAD, '3', 's' }, // KP_3
+  { KEYCODE_KEYPAD, '4', 't' }, // KP_4
+  { KEYCODE_KEYPAD, '5', 'u' }, // KP_5
+  { KEYCODE_KEYPAD, '6', 'v' }, // KP_6
+  { KEYCODE_KEYPAD, '7', 'w' }, // KP_7
+  { KEYCODE_KEYPAD, '8', 'x' }, // KP_8
+  { KEYCODE_KEYPAD, '9', 'y' }, // KP_9
+  { KEYCODE_KEYPAD, '*', 'j' }, // KP_MULT
+  { KEYCODE_KEYPAD, '+', 'k' }, // KP_PLUS
+  { KEYCODE_KEYPAD, ',', 'l' }, // KP_COMMA
+  { KEYCODE_KEYPAD, '-', 'm' }, // KP_MINUS
+  { KEYCODE_KEYPAD, '.', 'n' }, // KP_PERIOD
+  { KEYCODE_KEYPAD, '/', 'o' }, // KP_DIVIDE
+  { KEYCODE_KEYPAD, '\n', 'M' }, // KP_ENTER
+  { KEYCODE_KEYPAD, '=', 'X' }, // KP_EQUAL
+};
+
+void vterm_keyboard_key(VTerm *vt, VTermKey key, VTermModifier mod)
+{
+  if(key == VTERM_KEY_NONE)
+    return;
+
+  keycodes_s k;
+  if(key < VTERM_KEY_FUNCTION_0) {
+    if(key >= sizeof(keycodes)/sizeof(keycodes[0]))
+      return;
+    k = keycodes[key];
+  }
+  else if(key >= VTERM_KEY_FUNCTION_0 && key <= VTERM_KEY_FUNCTION_MAX) {
+    if((key - VTERM_KEY_FUNCTION_0) >= sizeof(keycodes_fn)/sizeof(keycodes_fn[0]))
+      return;
+    k = keycodes_fn[key - VTERM_KEY_FUNCTION_0];
+  }
+  else if(key >= VTERM_KEY_KP_0) {
+    if((key - VTERM_KEY_KP_0) >= sizeof(keycodes_kp)/sizeof(keycodes_kp[0]))
+      return;
+    k = keycodes_kp[key - VTERM_KEY_KP_0];
+  }
+
+  switch(k.type) {
+  case KEYCODE_NONE:
+    break;
+
+  case KEYCODE_TAB:
+    /* Shift-Tab is CSI Z but plain Tab is 0x09 */
+    if(mod == VTERM_MOD_SHIFT)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "Z");
+    else if(mod & VTERM_MOD_SHIFT)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%dZ", mod+1);
+    else
+      goto case_LITERAL;
+    break;
+
+  case KEYCODE_ENTER:
+    /* Enter is CRLF in newline mode, but just LF in linefeed */
+    if(vt->state->mode.newline)
+      vterm_push_output_sprintf(vt, "\r\n");
+    else
+      goto case_LITERAL;
+    break;
+
+  case KEYCODE_LITERAL: case_LITERAL:
+    if(mod & (VTERM_MOD_SHIFT|VTERM_MOD_CTRL))
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%du", k.literal, mod+1);
+    else
+      vterm_push_output_sprintf(vt, mod & VTERM_MOD_ALT ? ESC_S "%c" : "%c", k.literal);
+    break;
+
+  case KEYCODE_SS3: case_SS3:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_SS3, "%c", k.literal);
+    else
+      goto case_CSI;
+    break;
+
+  case KEYCODE_CSI: case_CSI:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%c", k.literal);
+    else
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "1;%d%c", mod + 1, k.literal);
+    break;
+
+  case KEYCODE_CSINUM:
+    if(mod == 0)
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d%c", k.csinum, k.literal);
+    else
+      vterm_push_output_sprintf_ctrl(vt, C1_CSI, "%d;%d%c", k.csinum, mod + 1, k.literal);
+    break;
+
+  case KEYCODE_CSI_CURSOR:
+    if(vt->state->mode.cursor)
+      goto case_SS3;
+    else
+      goto case_CSI;
+
+  case KEYCODE_KEYPAD:
+    if(vt->state->mode.keypad) {
+      k.literal = k.csinum;
+      goto case_SS3;
+    }
+    else
+      goto case_LITERAL;
+  }
+}
+
+void vterm_keyboard_start_paste(VTerm *vt)
+{
+  if(vt->state->mode.bracketpaste)
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "200~");
+}
+
+void vterm_keyboard_end_paste(VTerm *vt)
+{
+  if(vt->state->mode.bracketpaste)
+    vterm_push_output_sprintf_ctrl(vt, C1_CSI, "201~");
+}
diff --git a/src/mouse.c b/src/mouse.c
new file mode 100644
index 0000000..9962e4f
--- /dev/null
+++ b/src/mouse.c
@@ -0,0 +1,96 @@
+#include "vterm_internal.h"
+
+#include "utf8.h"
+
+static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row)
+{
+  modifiers <<= 2;
+
+  switch(state->mouse_protocol) {
+  case MOUSE_X10:
+    if(col + 0x21 > 0xff)
+      col = 0xff - 0x21;
+    if(row + 0x21 > 0xff)
+      row = 0xff - 0x21;
+
+    if(!pressed)
+      code = 3;
+
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c",
+        (code | modifiers) + 0x20, col + 0x21, row + 0x21);
+    break;
+
+  case MOUSE_UTF8:
+    {
+      char utf8[18]; size_t len = 0;
+
+      if(!pressed)
+        code = 3;
+
+      len += fill_utf8((code | modifiers) + 0x20, utf8 + len);
+      len += fill_utf8(col + 0x21, utf8 + len);
+      len += fill_utf8(row + 0x21, utf8 + len);
+      utf8[len] = 0;
+
+      vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8);
+    }
+    break;
+
+  case MOUSE_SGR:
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c",
+        code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm');
+    break;
+
+  case MOUSE_RXVT:
+    if(!pressed)
+      code = 3;
+
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM",
+        code | modifiers, col + 1, row + 1);
+    break;
+  }
+}
+
+void vterm_mouse_move(VTerm *vt, int row, int col, VTermModifier mod)
+{
+  VTermState *state = vt->state;
+
+  if(col == state->mouse_col && row == state->mouse_row)
+    return;
+
+  state->mouse_col = col;
+  state->mouse_row = row;
+
+  if((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) ||
+     (state->mouse_flags & MOUSE_WANT_MOVE)) {
+    int button = state->mouse_buttons & 0x01 ? 1 :
+                 state->mouse_buttons & 0x02 ? 2 :
+                 state->mouse_buttons & 0x04 ? 3 : 4;
+    output_mouse(state, button-1 + 0x20, 1, mod, col, row);
+  }
+}
+
+void vterm_mouse_button(VTerm *vt, int button, bool pressed, VTermModifier mod)
+{
+  VTermState *state = vt->state;
+
+  int old_buttons = state->mouse_buttons;
+
+  if(button > 0 && button <= 3) {
+    if(pressed)
+      state->mouse_buttons |= (1 << (button-1));
+    else
+      state->mouse_buttons &= ~(1 << (button-1));
+  }
+
+  /* Most of the time we don't get button releases from 4/5 */
+  if(state->mouse_buttons == old_buttons && button < 4)
+    return;
+
+  if(button < 4) {
+    output_mouse(state, button-1, pressed, mod, state->mouse_col, state->mouse_row);
+  }
+  else if(button < 6) {
+    output_mouse(state, button-4 + 0x40, pressed, mod, state->mouse_col, state->mouse_row);
+  }
+}
diff --git a/src/parser.c b/src/parser.c
index 13bbc21..a01cd71 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -3,218 +3,154 @@
 #include <stdio.h>
 #include <string.h>
 
-#define CSI_ARGS_MAX 16
-#define CSI_LEADER_MAX 16
-#define CSI_INTERMED_MAX 16
+#undef DEBUG_PARSER
+
+static bool is_intermed(unsigned char c)
+{
+  return c >= 0x20 && c <= 0x2f;
+}
 
 static void do_control(VTerm *vt, unsigned char control)
 {
-  if(vt->parser_callbacks && vt->parser_callbacks->control)
-    if((*vt->parser_callbacks->control)(control, vt->cbdata))
+  if(vt->parser.callbacks && vt->parser.callbacks->control)
+    if((*vt->parser.callbacks->control)(control, vt->parser.cbdata))
       return;
 
-  fprintf(stderr, "libvterm: Unhandled control 0x%02x\n", control);
+  DEBUG_LOG("libvterm: Unhandled control 0x%02x\n", control);
 }
 
-static void do_string_csi(VTerm *vt, const char *args, size_t arglen, char command)
+static void do_csi(VTerm *vt, char command)
 {
-  int i = 0;
-
-  int leaderlen = 0;
-  char leader[CSI_LEADER_MAX];
-
-  // Extract leader bytes 0x3c to 0x3f
-  for( ; i < arglen; i++) {
-    if(args[i] < 0x3c || args[i] > 0x3f)
-      break;
-    if(leaderlen < CSI_LEADER_MAX-1)
-      leader[leaderlen++] = args[i];
+#ifdef DEBUG_PARSER
+  printf("Parsed CSI args as:\n", arglen, args);
+  printf(" leader: %s\n", vt->parser.csi_leader);
+  for(int argi = 0; argi < vt->parser.csi_argi; argi++) {
+    printf(" %lu", CSI_ARG(vt->parser.csi_args[argi]));
+    if(!CSI_ARG_HAS_MORE(vt->parser.csi_args[argi]))
+      printf("\n");
+  printf(" intermed: %s\n", vt->parser.intermed);
   }
+#endif
 
-  leader[leaderlen] = 0;
-
-  int argcount = 1; // Always at least 1 arg
-
-  for( ; i < arglen; i++)
-    if(args[i] == 0x3b || args[i] == 0x3a) // ; or :
-      argcount++;
-
-  /* TODO: Consider if these buffers should live in the VTerm struct itself */
-  long csi_args[CSI_ARGS_MAX];
-  if(argcount > CSI_ARGS_MAX)
-    argcount = CSI_ARGS_MAX;
-
-  int argi;
-  for(argi = 0; argi < argcount; argi++)
-    csi_args[argi] = CSI_ARG_MISSING;
-
-  argi = 0;
-  for(i = leaderlen; i < arglen && argi < argcount; i++) {
-    switch(args[i]) {
-    case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
-    case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
-      if(csi_args[argi] == CSI_ARG_MISSING)
-        csi_args[argi] = 0;
-      csi_args[argi] *= 10;
-      csi_args[argi] += args[i] - '0';
-      break;
-    case 0x3a:
-      csi_args[argi] |= CSI_ARG_FLAG_MORE;
-      /* FALLTHROUGH */
-    case 0x3b:
-      argi++;
-      break;
-    default:
-      goto done_leader;
-    }
-  }
-done_leader: ;
-
-  int intermedlen = 0;
-  char intermed[CSI_INTERMED_MAX];
-
-  for( ; i < arglen; i++) {
-    if((args[i] & 0xf0) != 0x20)
-      break;
-
-    if(intermedlen < CSI_INTERMED_MAX-1)
-      intermed[intermedlen++] = args[i];
-  }
-
-  intermed[intermedlen] = 0;
-
-  if(i < arglen) {
-    fprintf(stderr, "libvterm: TODO unhandled CSI bytes \"%.*s\"\n", (int)(arglen - i), args + i);
-  }
-
-  //printf("Parsed CSI args %.*s as:\n", arglen, args);
-  //printf(" leader: %s\n", leader);
-  //for(argi = 0; argi < argcount; argi++) {
-  //  printf(" %lu", CSI_ARG(csi_args[argi]));
-  //  if(!CSI_ARG_HAS_MORE(csi_args[argi]))
-  //    printf("\n");
-  //printf(" intermed: %s\n", intermed);
-  //}
-
-  if(vt->parser_callbacks && vt->parser_callbacks->csi)
-    if((*vt->parser_callbacks->csi)(leaderlen ? leader : NULL, csi_args, argcount, intermedlen ? intermed : NULL, command, vt->cbdata))
+  if(vt->parser.callbacks && vt->parser.callbacks->csi)
+    if((*vt->parser.callbacks->csi)(
+          vt->parser.csi_leaderlen ? vt->parser.csi_leader : NULL, 
+          vt->parser.csi_args,
+          vt->parser.csi_argi,
+          vt->parser.intermedlen ? vt->parser.intermed : NULL,
+          command,
+          vt->parser.cbdata))
       return;
 
-  fprintf(stderr, "libvterm: Unhandled CSI %.*s %c\n", (int)arglen, args, command);
+  DEBUG_LOG("libvterm: Unhandled CSI %c\n", command);
+}
+
+static void do_escape(VTerm *vt, char command)
+{
+  char seq[INTERMED_MAX+1];
+
+  size_t len = vt->parser.intermedlen;
+  strncpy(seq, vt->parser.intermed, len);
+  seq[len++] = command;
+  seq[len]   = 0;
+
+  if(vt->parser.callbacks && vt->parser.callbacks->escape)
+    if((*vt->parser.callbacks->escape)(seq, len, vt->parser.cbdata))
+      return;
+
+  DEBUG_LOG("libvterm: Unhandled escape ESC 0x%02x\n", command);
 }
 
 static void append_strbuffer(VTerm *vt, const char *str, size_t len)
 {
-  if(len > vt->strbuffer_len - vt->strbuffer_cur) {
-    len = vt->strbuffer_len - vt->strbuffer_cur;
-    fprintf(stderr, "Truncating strbuffer preserve to %zd bytes\n", len);
+  if(len > vt->parser.strbuffer_len - vt->parser.strbuffer_cur) {
+    len = vt->parser.strbuffer_len - vt->parser.strbuffer_cur;
+    DEBUG_LOG("Truncating strbuffer preserve to %zd bytes\n", len);
   }
 
   if(len > 0) {
-    strncpy(vt->strbuffer + vt->strbuffer_cur, str, len);
-    vt->strbuffer_cur += len;
+    strncpy(vt->parser.strbuffer + vt->parser.strbuffer_cur, str, len);
+    vt->parser.strbuffer_cur += len;
   }
 }
 
-static size_t do_string(VTerm *vt, const char *str_frag, size_t len)
+static void start_string(VTerm *vt, VTermParserStringType type)
 {
-  if(vt->strbuffer_cur) {
-    if(str_frag)
-      append_strbuffer(vt, str_frag, len);
+  vt->parser.stringtype = type;
 
-    str_frag = vt->strbuffer;
-    len = vt->strbuffer_cur;
+  vt->parser.strbuffer_cur = 0;
+}
+
+static void more_string(VTerm *vt, const char *str, size_t len)
+{
+  append_strbuffer(vt, str, len);
+}
+
+static void done_string(VTerm *vt, const char *str, size_t len)
+{
+  if(vt->parser.strbuffer_cur) {
+    if(str)
+      append_strbuffer(vt, str, len);
+
+    str = vt->parser.strbuffer;
+    len = vt->parser.strbuffer_cur;
   }
-  else if(!str_frag) {
-    fprintf(stderr, "parser.c: TODO: No strbuffer _and_ no final fragment???\n");
+  else if(!str) {
+    DEBUG_LOG("parser.c: TODO: No strbuffer _and_ no final fragment???\n");
     len = 0;
   }
 
-  vt->strbuffer_cur = 0;
+  switch(vt->parser.stringtype) {
+  case VTERM_PARSER_OSC:
+    if(vt->parser.callbacks && vt->parser.callbacks->osc)
+      if((*vt->parser.callbacks->osc)(str, len, vt->parser.cbdata))
+        return;
 
-  size_t eaten;
+    DEBUG_LOG("libvterm: Unhandled OSC %.*s\n", (int)len, str);
+    return;
 
-  switch(vt->parser_state) {
-  case NORMAL:
-    if(vt->parser_callbacks && vt->parser_callbacks->text)
-      if((eaten = (*vt->parser_callbacks->text)(str_frag, len, vt->cbdata)))
-        return eaten;
+  case VTERM_PARSER_DCS:
+    if(vt->parser.callbacks && vt->parser.callbacks->dcs)
+      if((*vt->parser.callbacks->dcs)(str, len, vt->parser.cbdata))
+        return;
 
-    fprintf(stderr, "libvterm: Unhandled text (%zu chars)\n", len);
-    return 0;
+    DEBUG_LOG("libvterm: Unhandled DCS %.*s\n", (int)len, str);
+    return;
 
-  case ESC:
-    if(len == 1 && str_frag[0] >= 0x40 && str_frag[0] < 0x60) {
-      // C1 emulations using 7bit clean
-      // ESC 0x40 == 0x80
-      do_control(vt, str_frag[0] + 0x40);
-      return 0;
-    }
-
-    if(vt->parser_callbacks && vt->parser_callbacks->escape)
-      if((*vt->parser_callbacks->escape)(str_frag, len, vt->cbdata))
-        return 0;
-
-    fprintf(stderr, "libvterm: Unhandled escape ESC 0x%02x\n", str_frag[len-1]);
-    return 0;
-
-  case CSI:
-    do_string_csi(vt, str_frag, len - 1, str_frag[len - 1]);
-    return 0;
-
-  case OSC:
-    if(vt->parser_callbacks && vt->parser_callbacks->osc)
-      if((*vt->parser_callbacks->osc)(str_frag, len, vt->cbdata))
-        return 0;
-
-    fprintf(stderr, "libvterm: Unhandled OSC %.*s\n", (int)len, str_frag);
-    return 0;
-
-  case DCS:
-    if(vt->parser_callbacks && vt->parser_callbacks->dcs)
-      if((*vt->parser_callbacks->dcs)(str_frag, len, vt->cbdata))
-        return 0;
-
-    fprintf(stderr, "libvterm: Unhandled DCS %.*s\n", (int)len, str_frag);
-    return 0;
-
-  case ESC_IN_OSC:
-  case ESC_IN_DCS:
-    fprintf(stderr, "libvterm: ARGH! Should never do_string() in ESC_IN_{OSC,DCS}\n");
-    return 0;
+  case VTERM_N_PARSER_TYPES:
+    return;
   }
-
-  return 0;
 }
 
-void vterm_push_bytes(VTerm *vt, const char *bytes, size_t len)
+size_t vterm_input_write(VTerm *vt, const char *bytes, size_t len)
 {
   size_t pos = 0;
   const char *string_start;
 
-  switch(vt->parser_state) {
+  switch(vt->parser.state) {
   case NORMAL:
+  case CSI_LEADER:
+  case CSI_ARGS:
+  case CSI_INTERMED:
+  case ESC:
     string_start = NULL;
     break;
-  case ESC:
-  case ESC_IN_OSC:
-  case ESC_IN_DCS:
-  case CSI:
-  case OSC:
-  case DCS:
+  case STRING:
+  case ESC_IN_STRING:
     string_start = bytes;
     break;
   }
 
-#define ENTER_STRING_STATE(st) do { vt->parser_state = st; string_start = bytes + pos + 1; } while(0)
-#define ENTER_NORMAL_STATE()   do { vt->parser_state = NORMAL; string_start = NULL; } while(0)
+#define ENTER_STRING_STATE(st) do { vt->parser.state = STRING; string_start = bytes + pos + 1; } while(0)
+#define ENTER_STATE(st)        do { vt->parser.state = st; string_start = NULL; } while(0)
+#define ENTER_NORMAL_STATE()   ENTER_STATE(NORMAL)
 
   for( ; pos < len; pos++) {
     unsigned char c = bytes[pos];
 
     if(c == 0x00 || c == 0x7f) { // NUL, DEL
-      if(vt->parser_state != NORMAL) {
-        append_strbuffer(vt, string_start, bytes + pos - string_start);
+      if(vt->parser.state >= STRING) {
+        more_string(vt, string_start, bytes + pos - string_start);
         string_start = bytes + pos + 1;
       }
       continue;
@@ -224,83 +160,131 @@
       continue;
     }
     else if(c == 0x1b) { // ESC
-      if(vt->parser_state == OSC)
-        vt->parser_state = ESC_IN_OSC;
-      else if(vt->parser_state == DCS)
-        vt->parser_state = ESC_IN_DCS;
+      vt->parser.intermedlen = 0;
+      if(vt->parser.state == STRING)
+        vt->parser.state = ESC_IN_STRING;
       else
-        ENTER_STRING_STATE(ESC);
+        ENTER_STATE(ESC);
       continue;
     }
     else if(c == 0x07 &&  // BEL, can stand for ST in OSC or DCS state
-            (vt->parser_state == OSC || vt->parser_state == DCS)) {
+            vt->parser.state == STRING) {
       // fallthrough
     }
     else if(c < 0x20) { // other C0
-      if(vt->parser_state != NORMAL)
-        append_strbuffer(vt, string_start, bytes + pos - string_start);
+      if(vt->parser.state >= STRING)
+        more_string(vt, string_start, bytes + pos - string_start);
       do_control(vt, c);
-      if(vt->parser_state != NORMAL)
+      if(vt->parser.state >= STRING)
         string_start = bytes + pos + 1;
       continue;
     }
     // else fallthrough
 
-    switch(vt->parser_state) {
-    case ESC_IN_OSC:
-    case ESC_IN_DCS:
+    switch(vt->parser.state) {
+    case ESC_IN_STRING:
       if(c == 0x5c) { // ST
-        switch(vt->parser_state) {
-          case ESC_IN_OSC: vt->parser_state = OSC; break;
-          case ESC_IN_DCS: vt->parser_state = DCS; break;
-          default: break;
-        }
-        do_string(vt, string_start, bytes + pos - string_start - 1);
+        vt->parser.state = STRING;
+        done_string(vt, string_start, bytes + pos - string_start - 1);
         ENTER_NORMAL_STATE();
         break;
       }
-      vt->parser_state = ESC;
-      string_start = bytes + pos;
+      vt->parser.state = ESC;
       // else fallthrough
 
     case ESC:
       switch(c) {
       case 0x50: // DCS
-        ENTER_STRING_STATE(DCS);
+        start_string(vt, VTERM_PARSER_DCS);
+        ENTER_STRING_STATE();
         break;
       case 0x5b: // CSI
-        ENTER_STRING_STATE(CSI);
+        vt->parser.csi_leaderlen = 0;
+        ENTER_STATE(CSI_LEADER);
         break;
       case 0x5d: // OSC
-        ENTER_STRING_STATE(OSC);
+        start_string(vt, VTERM_PARSER_OSC);
+        ENTER_STRING_STATE();
         break;
       default:
-        if(c >= 0x30 && c < 0x7f) {
-          /* +1 to pos because we want to include this command byte as well */
-          do_string(vt, string_start, bytes + pos - string_start + 1);
+        if(is_intermed(c)) {
+          if(vt->parser.intermedlen < INTERMED_MAX-1)
+            vt->parser.intermed[vt->parser.intermedlen++] = c;
+        }
+        else if(!vt->parser.intermedlen && c >= 0x40 && c < 0x60) {
+          do_control(vt, c + 0x40);
           ENTER_NORMAL_STATE();
         }
-        else if(c >= 0x20 && c < 0x30) {
-          /* intermediate byte */
+        else if(c >= 0x30 && c < 0x7f) {
+          do_escape(vt, c);
+          ENTER_NORMAL_STATE();
         }
         else {
-          fprintf(stderr, "TODO: Unhandled byte %02x in Escape\n", c);
+          DEBUG_LOG("TODO: Unhandled byte %02x in Escape\n", c);
         }
       }
       break;
 
-    case CSI:
-      if(c >= 0x40 && c <= 0x7f) {
-        /* +1 to pos because we want to include this command byte as well */
-        do_string(vt, string_start, bytes + pos - string_start + 1);
-        ENTER_NORMAL_STATE();
+    case CSI_LEADER:
+      /* Extract leader bytes 0x3c to 0x3f */
+      if(c >= 0x3c && c <= 0x3f) {
+        if(vt->parser.csi_leaderlen < CSI_LEADER_MAX-1)
+          vt->parser.csi_leader[vt->parser.csi_leaderlen++] = c;
+        break;
       }
+
+      /* else fallthrough */
+      vt->parser.csi_leader[vt->parser.csi_leaderlen] = 0;
+
+      vt->parser.csi_argi = 0;
+      vt->parser.csi_args[0] = CSI_ARG_MISSING;
+      vt->parser.state = CSI_ARGS;
+
+      /* fallthrough */
+    case CSI_ARGS:
+      /* Numerical value of argument */
+      if(c >= '0' && c <= '9') {
+        if(vt->parser.csi_args[vt->parser.csi_argi] == CSI_ARG_MISSING)
+          vt->parser.csi_args[vt->parser.csi_argi] = 0;
+        vt->parser.csi_args[vt->parser.csi_argi] *= 10;
+        vt->parser.csi_args[vt->parser.csi_argi] += c - '0';
+        break;
+      }
+      if(c == ':') {
+        vt->parser.csi_args[vt->parser.csi_argi] |= CSI_ARG_FLAG_MORE;
+        c = ';';
+      }
+      if(c == ';') {
+        vt->parser.csi_argi++;
+        vt->parser.csi_args[vt->parser.csi_argi] = CSI_ARG_MISSING;
+        break;
+      }
+
+      /* else fallthrough */
+      vt->parser.csi_argi++;
+      vt->parser.intermedlen = 0;
+      vt->parser.state = CSI_INTERMED;
+    case CSI_INTERMED:
+      if(is_intermed(c)) {
+        if(vt->parser.intermedlen < INTERMED_MAX-1)
+          vt->parser.intermed[vt->parser.intermedlen++] = c;
+        break;
+      }
+      else if(c == 0x1b) {
+        /* ESC in CSI cancels */
+      }
+      else if(c >= 0x40 && c <= 0x7e) {
+        vt->parser.intermed[vt->parser.intermedlen] = 0;
+        do_csi(vt, c);
+      }
+      /* else was invalid CSI */
+
+      ENTER_NORMAL_STATE();
       break;
 
-    case OSC:
-    case DCS:
+    case STRING:
       if(c == 0x07 || (c == 0x9c && !vt->mode.utf8)) {
-        do_string(vt, string_start, bytes + pos - string_start);
+        done_string(vt, string_start, bytes + pos - string_start);
         ENTER_NORMAL_STATE();
       }
       break;
@@ -309,13 +293,15 @@
       if(c >= 0x80 && c < 0xa0 && !vt->mode.utf8) {
         switch(c) {
         case 0x90: // DCS
-          ENTER_STRING_STATE(DCS);
+          start_string(vt, VTERM_PARSER_DCS);
+          ENTER_STRING_STATE();
           break;
         case 0x9b: // CSI
-          ENTER_STRING_STATE(CSI);
+          ENTER_STATE(CSI_LEADER);
           break;
         case 0x9d: // OSC
-          ENTER_STRING_STATE(OSC);
+          start_string(vt, VTERM_PARSER_OSC);
+          ENTER_STRING_STATE();
           break;
         default:
           do_control(vt, c);
@@ -323,22 +309,32 @@
         }
       }
       else {
-        size_t text_eaten = do_string(vt, bytes + pos, len - pos);
+        size_t eaten = 0;
+        if(vt->parser.callbacks && vt->parser.callbacks->text)
+          eaten = (*vt->parser.callbacks->text)(bytes + pos, len - pos, vt->parser.cbdata);
 
-        if(text_eaten == 0) {
-          string_start = bytes + pos;
-          goto pause;
+        if(!eaten) {
+          DEBUG_LOG("libvterm: Text callback did not consume any input\n");
+          /* force it to make progress */
+          eaten = 1;
         }
 
-        pos += (text_eaten - 1); // we'll ++ it again in a moment
+        pos += (eaten - 1); // we'll ++ it again in a moment
       }
       break;
     }
   }
 
-pause:
-  if(string_start && string_start < len + bytes) {
-    size_t remaining = len - (string_start - bytes);
-    append_strbuffer(vt, string_start, remaining);
-  }
+  return len;
+}
+
+void vterm_parser_set_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user)
+{
+  vt->parser.callbacks = callbacks;
+  vt->parser.cbdata = user;
+}
+
+void *vterm_parser_get_cbdata(VTerm *vt)
+{
+  return vt->parser.cbdata;
 }
diff --git a/src/pen.c b/src/pen.c
index fb8c8e3..7488203 100644
--- a/src/pen.c
+++ b/src/pen.c
@@ -2,7 +2,15 @@
 
 #include <stdio.h>
 
-static const VTermColor ansi_colors[] = {
+/**
+ * Structure used to store RGB triples without the additional metadata stored in
+ * VTermColor.
+ */
+typedef struct {
+  uint8_t red, green, blue;
+} VTermRGB;
+
+static const VTermRGB ansi_colors[] = {
   /* R    G    B */
   {   0,   0,   0 }, // black
   { 224,   0,   0 }, // red
@@ -33,60 +41,75 @@
   0x85, 0x90, 0x9B, 0xA6, 0xB1, 0xBC, 0xC7, 0xD2, 0xDD, 0xE8, 0xF3, 0xFF,
 };
 
-static void lookup_colour_ansi(const VTermState *state, long index, VTermColor *col)
+static void lookup_default_colour_ansi(long idx, VTermColor *col)
 {
-  if(index >= 0 && index < 16) {
-    *col = state->colors[index];
+  if (idx >= 0 && idx < 16) {
+    vterm_color_rgb(
+        col,
+        ansi_colors[idx].red, ansi_colors[idx].green, ansi_colors[idx].blue);
   }
 }
 
-static void lookup_colour_palette(const VTermState *state, long index, VTermColor *col)
+static bool lookup_colour_ansi(const VTermState *state, long index, VTermColor *col)
+{
+  if(index >= 0 && index < 16) {
+    *col = state->colors[index];
+    return true;
+  }
+
+  return false;
+}
+
+static bool lookup_colour_palette(const VTermState *state, long index, VTermColor *col)
 {
   if(index >= 0 && index < 16) {
     // Normal 8 colours or high intensity - parse as palette 0
-    lookup_colour_ansi(state, index, col);
+    return lookup_colour_ansi(state, index, col);
   }
   else if(index >= 16 && index < 232) {
     // 216-colour cube
     index -= 16;
 
-    col->blue  = ramp6[index     % 6];
-    col->green = ramp6[index/6   % 6];
-    col->red   = ramp6[index/6/6 % 6];
+    vterm_color_rgb(col, ramp6[index/6/6 % 6],
+                         ramp6[index/6   % 6],
+                         ramp6[index     % 6]);
+
+    return true;
   }
   else if(index >= 232 && index < 256) {
     // 24 greyscales
     index -= 232;
 
-    col->red   = ramp24[index];
-    col->green = ramp24[index];
-    col->blue  = ramp24[index];
+    vterm_color_rgb(col, ramp24[index], ramp24[index], ramp24[index]);
+
+    return true;
   }
+
+  return false;
 }
 
-static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, VTermColor *col, int *index)
+static int lookup_colour(const VTermState *state, int palette, const long args[], int argcount, VTermColor *col)
 {
   switch(palette) {
   case 2: // RGB mode - 3 args contain colour values directly
     if(argcount < 3)
       return argcount;
 
-    col->red   = CSI_ARG(args[0]);
-    col->green = CSI_ARG(args[1]);
-    col->blue  = CSI_ARG(args[2]);
+    vterm_color_rgb(col, CSI_ARG(args[0]), CSI_ARG(args[1]), CSI_ARG(args[2]));
 
     return 3;
 
   case 5: // XTerm 256-colour mode
-    if(index)
-      *index = CSI_ARG_OR(args[0], -1);
+    if (!argcount || CSI_ARG_IS_MISSING(args[0])) {
+      return argcount ? 1 : 0;
+    }
 
-    lookup_colour_palette(state, argcount ? CSI_ARG_OR(args[0], -1) : -1, col);
+    vterm_color_indexed(col, args[0]);
 
     return argcount ? 1 : 0;
 
   default:
-    fprintf(stderr, "Unrecognised colour palette %d\n", palette);
+    DEBUG_LOG("Unrecognised colour palette %d\n", palette);
     return 0;
   }
 }
@@ -97,7 +120,7 @@
 {
 #ifdef DEBUG
   if(type != vterm_get_attr_type(attr)) {
-    fprintf(stderr, "Cannot set attr %d as it has type %d, not type %d\n",
+    DEBUG_LOG("Cannot set attr %d as it has type %d, not type %d\n",
         attr, vterm_get_attr_type(attr), type);
     return;
   }
@@ -128,7 +151,7 @@
 {
   VTermColor *colp = (attr == VTERM_ATTR_BACKGROUND) ? &state->pen.bg : &state->pen.fg;
 
-  lookup_colour_ansi(state, col, colp);
+  vterm_color_indexed(colp, col);
 
   setpenattr_col(state, attr, *colp);
 }
@@ -136,11 +159,12 @@
 INTERNAL void vterm_state_newpen(VTermState *state)
 {
   // 90% grey so that pure white is brighter
-  state->default_fg.red = state->default_fg.green = state->default_fg.blue = 240;
-  state->default_bg.red = state->default_bg.green = state->default_bg.blue = 0;
+  vterm_color_rgb(&state->default_fg, 240, 240, 240);
+  vterm_color_rgb(&state->default_bg, 0, 0, 0);
+  vterm_state_set_default_colors(state, &state->default_fg, &state->default_bg);
 
   for(int col = 0; col < 16; col++)
-    state->colors[col] = ansi_colors[col];
+    lookup_default_colour_ansi(col, &state->colors[col]);
 }
 
 INTERNAL void vterm_state_resetpen(VTermState *state)
@@ -153,8 +177,6 @@
   state->pen.strike = 0;    setpenattr_bool(state, VTERM_ATTR_STRIKE, 0);
   state->pen.font = 0;      setpenattr_int( state, VTERM_ATTR_FONT, 0);
 
-  state->fg_index = -1;
-  state->bg_index = -1;
   state->pen.fg = state->default_fg;  setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->default_fg);
   state->pen.bg = state->default_bg;  setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->default_bg);
 }
@@ -179,6 +201,26 @@
   }
 }
 
+int vterm_color_is_equal(const VTermColor *a, const VTermColor *b)
+{
+  /* First make sure that the two colours are of the same type (RGB/Indexed) */
+  if (a->type != b->type) {
+    return false;
+  }
+
+  /* Depending on the type inspect the corresponding members */
+  if (VTERM_COLOR_IS_INDEXED(a)) {
+    return a->indexed.idx == b->indexed.idx;
+  }
+  else if (VTERM_COLOR_IS_RGB(a)) {
+    return    (a->rgb.red   == b->rgb.red)
+           && (a->rgb.green == b->rgb.green)
+           && (a->rgb.blue  == b->rgb.blue);
+  }
+
+  return 0;
+}
+
 void vterm_state_get_default_colors(const VTermState *state, VTermColor *default_fg, VTermColor *default_bg)
 {
   *default_fg = state->default_fg;
@@ -192,8 +234,15 @@
 
 void vterm_state_set_default_colors(VTermState *state, const VTermColor *default_fg, const VTermColor *default_bg)
 {
+  /* Copy the given colors */
   state->default_fg = *default_fg;
   state->default_bg = *default_bg;
+
+  /* Make sure the correct type flags are set */
+  state->default_fg.type = (state->default_fg.type & ~VTERM_COLOR_DEFAULT_MASK)
+                         | VTERM_COLOR_DEFAULT_FG;
+  state->default_bg.type = (state->default_bg.type & ~VTERM_COLOR_DEFAULT_MASK)
+                         | VTERM_COLOR_DEFAULT_BG;
 }
 
 void vterm_state_set_palette_color(VTermState *state, int index, const VTermColor *col)
@@ -202,6 +251,14 @@
     state->colors[index] = *col;
 }
 
+void vterm_state_convert_color_to_rgb(const VTermState *state, VTermColor *col)
+{
+  if (VTERM_COLOR_IS_INDEXED(col)) { /* Convert indexed colors to RGB */
+    lookup_colour_palette(state, col->indexed.idx, col);
+  }
+  col->type &= VTERM_COLOR_TYPE_MASK; /* Reset any metadata but the type */
+}
+
 void vterm_state_set_bold_highbright(VTermState *state, int bold_is_highbright)
 {
   state->bold_is_highbright = bold_is_highbright;
@@ -226,12 +283,14 @@
       vterm_state_resetpen(state);
       break;
 
-    case 1: // Bold on
+    case 1: { // Bold on
+      const VTermColor *fg = &state->pen.fg;
       state->pen.bold = 1;
       setpenattr_bool(state, VTERM_ATTR_BOLD, 1);
-      if(state->fg_index > -1 && state->fg_index < 8 && state->bold_is_highbright)
-        set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, state->fg_index + (state->pen.bold ? 8 : 0));
+      if(!VTERM_COLOR_IS_DEFAULT_FG(fg) && VTERM_COLOR_IS_INDEXED(fg) && fg->indexed.idx < 8 && state->bold_is_highbright)
+        set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, fg->indexed.idx + (state->pen.bold ? 8 : 0));
       break;
+    }
 
     case 3: // Italic on
       state->pen.italic = 1;
@@ -302,22 +361,19 @@
     case 30: case 31: case 32: case 33:
     case 34: case 35: case 36: case 37: // Foreground colour palette
       value = CSI_ARG(args[argi]) - 30;
-      state->fg_index = value;
       if(state->pen.bold && state->bold_is_highbright)
         value += 8;
       set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value);
       break;
 
     case 38: // Foreground colour alternative palette
-      state->fg_index = -1;
       if(argcount - argi < 1)
         return;
-      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.fg, &state->fg_index);
+      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.fg);
       setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg);
       break;
 
     case 39: // Foreground colour default
-      state->fg_index = -1;
       state->pen.fg = state->default_fg;
       setpenattr_col(state, VTERM_ATTR_FOREGROUND, state->pen.fg);
       break;
@@ -325,20 +381,17 @@
     case 40: case 41: case 42: case 43:
     case 44: case 45: case 46: case 47: // Background colour palette
       value = CSI_ARG(args[argi]) - 40;
-      state->bg_index = value;
       set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value);
       break;
 
     case 48: // Background colour alternative palette
-      state->bg_index = -1;
       if(argcount - argi < 1)
         return;
-      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.bg, &state->bg_index);
+      argi += 1 + lookup_colour(state, CSI_ARG(args[argi+1]), args+argi+2, argcount-argi-2, &state->pen.bg);
       setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg);
       break;
 
     case 49: // Default background
-      state->bg_index = -1;
       state->pen.bg = state->default_bg;
       setpenattr_col(state, VTERM_ATTR_BACKGROUND, state->pen.bg);
       break;
@@ -346,14 +399,12 @@
     case 90: case 91: case 92: case 93:
     case 94: case 95: case 96: case 97: // Foreground colour high-intensity palette
       value = CSI_ARG(args[argi]) - 90 + 8;
-      state->fg_index = value;
       set_pen_col_ansi(state, VTERM_ATTR_FOREGROUND, value);
       break;
 
     case 100: case 101: case 102: case 103:
     case 104: case 105: case 106: case 107: // Background colour high-intensity palette
       value = CSI_ARG(args[argi]) - 100 + 8;
-      state->bg_index = value;
       set_pen_col_ansi(state, VTERM_ATTR_BACKGROUND, value);
       break;
 
@@ -363,12 +414,45 @@
     }
 
     if(!done)
-      fprintf(stderr, "libvterm: Unhandled CSI SGR %lu\n", arg);
+      DEBUG_LOG("libvterm: Unhandled CSI SGR %lu\n", arg);
 
     while(CSI_ARG_HAS_MORE(args[argi++]));
   }
 }
 
+static int vterm_state_getpen_color(const VTermColor *col, int argi, long args[], int fg)
+{
+    /* Do nothing if the given color is the default color */
+    if (( fg && VTERM_COLOR_IS_DEFAULT_FG(col)) ||
+        (!fg && VTERM_COLOR_IS_DEFAULT_BG(col))) {
+        return argi;
+    }
+
+    /* Decide whether to send an indexed color or an RGB color */
+    if (VTERM_COLOR_IS_INDEXED(col)) {
+        const uint8_t idx = col->indexed.idx;
+        if (idx < 8) {
+            args[argi++] = (idx + (fg ? 30 : 40));
+        }
+        else if (idx < 16) {
+            args[argi++] = (idx - 8 + (fg ? 90 : 100));
+        }
+        else {
+            args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48);
+            args[argi++] = CSI_ARG_FLAG_MORE | 5;
+            args[argi++] = idx;
+        }
+    }
+    else if (VTERM_COLOR_IS_RGB(col)) {
+        args[argi++] = CSI_ARG_FLAG_MORE | (fg ? 38 : 48);
+        args[argi++] = CSI_ARG_FLAG_MORE | 2;
+        args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.red;
+        args[argi++] = CSI_ARG_FLAG_MORE | col->rgb.green;
+        args[argi++] = col->rgb.blue;
+    }
+    return argi;
+}
+
 INTERNAL int vterm_state_getpen(VTermState *state, long args[], int argcount)
 {
   int argi = 0;
@@ -397,25 +481,9 @@
   if(state->pen.underline == 2)
     args[argi++] = 21;
 
-  if(state->fg_index >= 0 && state->fg_index < 8)
-    args[argi++] = 30 + state->fg_index;
-  else if(state->fg_index >= 8 && state->fg_index < 16)
-    args[argi++] = 90 + state->fg_index - 8;
-  else if(state->fg_index >= 16 && state->fg_index < 256) {
-    args[argi++] = CSI_ARG_FLAG_MORE|38;
-    args[argi++] = CSI_ARG_FLAG_MORE|5;
-    args[argi++] = state->fg_index;
-  }
+  argi = vterm_state_getpen_color(&state->pen.fg, argi, args, true);
 
-  if(state->bg_index >= 0 && state->bg_index < 8)
-    args[argi++] = 40 + state->bg_index;
-  else if(state->bg_index >= 8 && state->bg_index < 16)
-    args[argi++] = 100 + state->bg_index - 8;
-  else if(state->bg_index >= 16 && state->bg_index < 256) {
-    args[argi++] = CSI_ARG_FLAG_MORE|48;
-    args[argi++] = CSI_ARG_FLAG_MORE|5;
-    args[argi++] = state->bg_index;
-  }
+  argi = vterm_state_getpen_color(&state->pen.bg, argi, args, false);
 
   return argi;
 }
@@ -458,6 +526,9 @@
   case VTERM_ATTR_BACKGROUND:
     val->color = state->pen.bg;
     return 1;
+
+  case VTERM_N_ATTRS:
+    return 0;
   }
 
   return 0;
diff --git a/src/screen.c b/src/screen.c
index c4de59e..1d4d86c 100644
--- a/src/screen.c
+++ b/src/screen.c
@@ -149,7 +149,7 @@
     return;
 
   default:
-    fprintf(stderr, "TODO: Maybe merge damage for level %d\n", screen->damage_merge);
+    DEBUG_LOG("TODO: Maybe merge damage for level %d\n", screen->damage_merge);
     return;
   }
 
@@ -266,7 +266,7 @@
 {
   VTermScreen *screen = user;
 
-  for(int row = rect.start_row; row < rect.end_row; row++) {
+  for(int row = rect.start_row; row < screen->state->rows && row < rect.end_row; row++) {
     const VTermLineInfo *info = vterm_state_get_lineinfo(screen->state, row);
 
     for(int col = rect.start_col; col < rect.end_col; col++) {
@@ -304,10 +304,10 @@
 {
   VTermScreen *screen = user;
 
-  vterm_scroll_rect(rect, downward, rightward,
-      moverect_internal, erase_internal, screen);
-
   if(screen->damage_merge != VTERM_DAMAGE_SCROLL) {
+    vterm_scroll_rect(rect, downward, rightward,
+        moverect_internal, erase_internal, screen);
+
     vterm_screen_flush_damage(screen);
 
     vterm_scroll_rect(rect, downward, rightward,
@@ -340,10 +340,14 @@
     screen->pending_scroll_rightward = rightward;
   }
 
+  vterm_scroll_rect(rect, downward, rightward,
+      moverect_internal, erase_internal, screen);
+
   if(screen->damaged.start_row == -1)
     return 1;
 
   if(rect_contains(&rect, &screen->damaged)) {
+    /* Scroll region entirely contains the damage; just move it */
     vterm_rect_move(&screen->damaged, -downward, -rightward);
     rect_clip(&screen->damaged, &rect);
   }
@@ -373,7 +377,7 @@
     }
   }
   else {
-    fprintf(stderr, "TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n",
+    DEBUG_LOG("TODO: Just flush and redo damaged=" STRFrect " rect=" STRFrect "\n",
         ARGSrect(screen->damaged), ARGSrect(rect));
   }
 
@@ -422,6 +426,9 @@
   case VTERM_ATTR_BACKGROUND:
     screen->pen.bg = val->color;
     return 1;
+
+  case VTERM_N_ATTRS:
+    return 0;
   }
 
   return 0;
@@ -457,16 +464,6 @@
   return 1;
 }
 
-static int setmousefunc(VTermMouseFunc func, void *data, void *user)
-{
-  VTermScreen *screen = user;
-
-  if(screen->callbacks && screen->callbacks->setmousefunc)
-    return (*screen->callbacks->setmousefunc)(func, data, screen->cbdata);
-
-  return 0;
-}
-
 static int bell(void *user)
 {
   VTermScreen *screen = user;
@@ -490,8 +487,10 @@
     // Fewer rows - determine if we're going to scroll at all, and if so, push
     // those lines to scrollback
     VTermPos pos = { 0, 0 };
+    VTermPos cursor = screen->state->pos;
+    // Find the first blank row after the cursor.
     for(pos.row = old_rows - 1; pos.row >= new_rows; pos.row--)
-      if(!vterm_screen_is_eol(screen, pos))
+      if(!vterm_screen_is_eol(screen, pos) || cursor.row == pos.row)
         break;
 
     int first_blank_row = pos.row + 1;
@@ -609,16 +608,15 @@
 }
 
 static VTermStateCallbacks state_cbs = {
-  .putglyph     = &putglyph,
-  .movecursor   = &movecursor,
-  .scrollrect   = &scrollrect,
-  .erase        = &erase,
-  .setpenattr   = &setpenattr,
-  .settermprop  = &settermprop,
-  .setmousefunc = &setmousefunc,
-  .bell         = &bell,
-  .resize       = &resize,
-  .setlineinfo  = &setlineinfo,
+  .putglyph    = &putglyph,
+  .movecursor  = &movecursor,
+  .scrollrect  = &scrollrect,
+  .erase       = &erase,
+  .setpenattr  = &setpenattr,
+  .settermprop = &settermprop,
+  .bell        = &bell,
+  .resize      = &resize,
+  .setlineinfo = &setlineinfo,
 };
 
 static VTermScreen *screen_new(VTerm *vt)
@@ -642,6 +640,9 @@
   screen->rows = rows;
   screen->cols = cols;
 
+  screen->callbacks = NULL;
+  screen->cbdata    = NULL;
+
   screen->buffers[0] = realloc_buffer(screen, NULL, rows, cols);
 
   screen->buffer = screen->buffers[0];
@@ -839,6 +840,21 @@
   screen->cbdata = user;
 }
 
+void *vterm_screen_get_cbdata(VTermScreen *screen)
+{
+  return screen->cbdata;
+}
+
+void vterm_screen_set_unrecognised_fallbacks(VTermScreen *screen, const VTermParserCallbacks *fallbacks, void *user)
+{
+  vterm_state_set_unrecognised_fallbacks(screen->state, fallbacks, user);
+}
+
+void *vterm_screen_get_unrecognised_fbdata(VTermScreen *screen)
+{
+  return vterm_state_get_unrecognised_fbdata(screen->state);
+}
+
 void vterm_screen_flush_damage(VTermScreen *screen)
 {
   if(screen->pending_scrollrect.start_row != -1) {
@@ -878,9 +894,9 @@
     return 1;
   if((attrs & VTERM_ATTR_FONT_MASK)       && (a->pen.font != b->pen.font))
     return 1;
-  if((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_equal(a->pen.fg, b->pen.fg))
+  if((attrs & VTERM_ATTR_FOREGROUND_MASK) && !vterm_color_is_equal(&a->pen.fg, &b->pen.fg))
     return 1;
-  if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_equal(a->pen.bg, b->pen.bg))
+  if((attrs & VTERM_ATTR_BACKGROUND_MASK) && !vterm_color_is_equal(&a->pen.bg, &b->pen.bg))
     return 1;
 
   return 0;
@@ -913,3 +929,8 @@
 
   return 1;
 }
+
+void vterm_screen_convert_color_to_rgb(const VTermScreen *screen, VTermColor *col)
+{
+  vterm_state_convert_color_to_rgb(screen->state, col);
+}
diff --git a/src/state.c b/src/state.c
index e42be53..68cc4f6 100644
--- a/src/state.c
+++ b/src/state.c
@@ -5,16 +5,10 @@
 
 #define strneq(a,b,n) (strncmp(a,b,n)==0)
 
-#include "utf8.h"
-
 #if defined(DEBUG) && DEBUG > 1
 # define DEBUG_GLYPH_COMBINE
 #endif
 
-#define MOUSE_WANT_CLICK 0x01
-#define MOUSE_WANT_DRAG  0x02
-#define MOUSE_WANT_MOVE  0x04
-
 /* Some convenient wrappers to make callback functions easier */
 
 static void putglyph(VTermState *state, const uint32_t chars[], int width, VTermPos pos)
@@ -31,7 +25,7 @@
     if((*state->callbacks->putglyph)(&info, pos, state->cbdata))
       return;
 
-  fprintf(stderr, "libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row);
+  DEBUG_LOG("libvterm: Unhandled putglyph U+%04x at (%d,%d)\n", chars[0], pos.col, pos.row);
 }
 
 static void updatecursor(VTermState *state, VTermPos *oldpos, int cancel_phantom)
@@ -63,6 +57,15 @@
   state->rows = vt->rows;
   state->cols = vt->cols;
 
+  state->mouse_col     = 0;
+  state->mouse_row     = 0;
+  state->mouse_buttons = 0;
+
+  state->mouse_protocol = MOUSE_X10;
+
+  state->callbacks = NULL;
+  state->cbdata    = NULL;
+
   vterm_state_newpen(state);
 
   state->bold_is_highbright = 0;
@@ -83,6 +86,18 @@
   if(!downward && !rightward)
     return;
 
+  int rows = rect.end_row - rect.start_row;
+  if(downward > rows)
+    downward = rows;
+  else if(downward < -rows)
+    downward = -rows;
+
+  int cols = rect.end_col - rect.start_col;
+  if(rightward > cols)
+    rightward = cols;
+  else if(rightward < -cols)
+    rightward = -cols;
+
   // Update lineinfo if full line
   if(rect.start_col == 0 && rect.end_col == state->cols && rightward == 0) {
     int height = rect.end_row - rect.start_row - abs(downward);
@@ -153,15 +168,37 @@
   return state->tabstops[col >> 3] & mask;
 }
 
+static int is_cursor_in_scrollregion(const VTermState *state)
+{
+  if(state->pos.row < state->scrollregion_top ||
+     state->pos.row >= SCROLLREGION_BOTTOM(state))
+    return 0;
+  if(state->pos.col < SCROLLREGION_LEFT(state) ||
+     state->pos.col >= SCROLLREGION_RIGHT(state))
+    return 0;
+
+  return 1;
+}
+
 static void tab(VTermState *state, int count, int direction)
 {
-  while(count--)
-    while(state->pos.col >= 0 && state->pos.col < THISROWWIDTH(state)-1) {
-      state->pos.col += direction;
+  while(count > 0) {
+    if(direction > 0) {
+      if(state->pos.col >= THISROWWIDTH(state)-1)
+        return;
 
-      if(is_col_tabstop(state, state->pos.col))
-        break;
+      state->pos.col++;
     }
+    else if(direction < 0) {
+      if(state->pos.col < 1)
+        return;
+
+      state->pos.col--;
+    }
+
+    if(is_col_tabstop(state, state->pos.col))
+      count--;
+  }
 }
 
 #define NO_FORCE 0
@@ -219,6 +256,12 @@
       codepoints, &npoints, state->gsingle_set ? 1 : len,
       bytes, &eaten, len);
 
+  /* There's a chance an encoding (e.g. UTF-8) hasn't found enough bytes yet
+   * for even a single codepoint
+   */
+  if(!npoints)
+    return eaten;
+
   if(state->gsingle_set && npoints)
     state->gsingle_set = 0;
 
@@ -262,7 +305,7 @@
       putglyph(state, state->combine_chars, state->combine_width, state->combine_pos);
     }
     else {
-      fprintf(stderr, "libvterm: TODO: Skip over split char+combining\n");
+      DEBUG_LOG("libvterm: TODO: Skip over split char+combining\n");
     }
   }
 
@@ -280,7 +323,14 @@
 
     for( ; i < glyph_ends; i++) {
       chars[i - glyph_starts] = codepoints[i];
-      width += vterm_unicode_width(codepoints[i]);
+      int this_width = vterm_unicode_width(codepoints[i]);
+#ifdef DEBUG
+      if(this_width < 0) {
+        fprintf(stderr, "Text with negative-width codepoint U+%04x\n", codepoints[i]);
+        abort();
+      }
+#endif
+      width += this_width;
     }
 
     chars[glyph_ends - glyph_starts] = 0;
@@ -343,6 +393,15 @@
 
   updatecursor(state, &oldpos, 0);
 
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after text: (%d,%d)\n",
+        state->pos.row, state->pos.col);
+    abort();
+  }
+#endif
+
   return eaten;
 }
 
@@ -424,104 +483,27 @@
     break;
 
   default:
+    if(state->fallbacks && state->fallbacks->control)
+      if((*state->fallbacks->control)(control, state->fbdata))
+        return 1;
+
     return 0;
   }
 
   updatecursor(state, &oldpos, 1);
 
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after Ctrl %02x: (%d,%d)\n",
+        control, state->pos.row, state->pos.col);
+    abort();
+  }
+#endif
+
   return 1;
 }
 
-static void output_mouse(VTermState *state, int code, int pressed, int modifiers, int col, int row)
-{
-  modifiers <<= 2;
-
-  switch(state->mouse_protocol) {
-  case MOUSE_X10:
-    if(col + 0x21 > 0xff)
-      col = 0xff - 0x21;
-    if(row + 0x21 > 0xff)
-      row = 0xff - 0x21;
-
-    if(!pressed)
-      code = 3;
-
-    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%c%c%c",
-        (code | modifiers) + 0x20, col + 0x21, row + 0x21);
-    break;
-
-  case MOUSE_UTF8:
-    {
-      char utf8[18]; size_t len = 0;
-
-      if(!pressed)
-        code = 3;
-
-      len += fill_utf8((code | modifiers) + 0x20, utf8 + len);
-      len += fill_utf8(col + 0x21, utf8 + len);
-      len += fill_utf8(row + 0x21, utf8 + len);
-      utf8[len] = 0;
-
-      vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "M%s", utf8);
-    }
-    break;
-
-  case MOUSE_SGR:
-    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "<%d;%d;%d%c",
-        code | modifiers, col + 1, row + 1, pressed ? 'M' : 'm');
-    break;
-
-  case MOUSE_RXVT:
-    if(!pressed)
-      code = 3;
-
-    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "%d;%d;%dM",
-        code | modifiers, col + 1, row + 1);
-    break;
-  }
-}
-
-static void mousefunc(int col, int row, int button, int pressed, int modifiers, void *data)
-{
-  VTermState *state = data;
-
-  int old_col     = state->mouse_col;
-  int old_row     = state->mouse_row;
-  int old_buttons = state->mouse_buttons;
-
-  state->mouse_col = col;
-  state->mouse_row = row;
-
-  if(button > 0 && button <= 3) {
-    if(pressed)
-      state->mouse_buttons |= (1 << (button-1));
-    else
-      state->mouse_buttons &= ~(1 << (button-1));
-  }
-
-  modifiers &= 0x7;
-
-
-  /* Most of the time we don't get button releases from 4/5 */
-  if(state->mouse_buttons != old_buttons || button >= 4) {
-    if(button < 4) {
-      output_mouse(state, button-1, pressed, modifiers, col, row);
-    }
-    else if(button < 6) {
-      output_mouse(state, button-4 + 0x40, pressed, modifiers, col, row);
-    }
-  }
-  else if(col != old_col || row != old_row) {
-    if((state->mouse_flags & MOUSE_WANT_DRAG && state->mouse_buttons) ||
-       (state->mouse_flags & MOUSE_WANT_MOVE)) {
-      int button = state->mouse_buttons & 0x01 ? 1 :
-                   state->mouse_buttons & 0x02 ? 2 :
-                   state->mouse_buttons & 0x04 ? 3 : 4;
-      output_mouse(state, button-1 + 0x20, 1, modifiers, col, row);
-    }
-  }
-}
-
 static int settermprop_bool(VTermState *state, VTermProp prop, int v)
 {
   VTermValue val = { .boolean = v };
@@ -722,7 +704,7 @@
     break;
 
   default:
-    fprintf(stderr, "libvterm: Unknown mode %d\n", num);
+    DEBUG_LOG("libvterm: Unknown mode %d\n", num);
     return;
   }
 }
@@ -774,26 +756,15 @@
   case 1000:
   case 1002:
   case 1003:
-    if(val) {
-      state->mouse_col     = 0;
-      state->mouse_row     = 0;
-      state->mouse_buttons = 0;
+    settermprop_int(state, VTERM_PROP_MOUSE,
+        !val          ? VTERM_PROP_MOUSE_NONE  :
+        (num == 1000) ? VTERM_PROP_MOUSE_CLICK :
+        (num == 1002) ? VTERM_PROP_MOUSE_DRAG  :
+                        VTERM_PROP_MOUSE_MOVE);
+    break;
 
-      state->mouse_flags = MOUSE_WANT_CLICK;
-      state->mouse_protocol = MOUSE_X10;
-
-      if(num == 1002)
-        state->mouse_flags |= MOUSE_WANT_DRAG;
-      if(num == 1003)
-        state->mouse_flags |= MOUSE_WANT_MOVE;
-    }
-    else {
-      state->mouse_flags = 0;
-    }
-
-    if(state->callbacks && state->callbacks->setmousefunc)
-      (*state->callbacks->setmousefunc)(val ? mousefunc : NULL, state, state->cbdata);
-
+  case 1004:
+    state->mode.report_focus = val;
     break;
 
   case 1005:
@@ -821,8 +792,12 @@
     savecursor(state, val);
     break;
 
+  case 2004:
+    state->mode.bracketpaste = val;
+    break;
+
   default:
-    fprintf(stderr, "libvterm: Unknown DEC mode %d\n", num);
+    DEBUG_LOG("libvterm: Unknown DEC mode %d\n", num);
     return;
   }
 }
@@ -872,6 +847,10 @@
       reply = state->mouse_flags == (MOUSE_WANT_CLICK|MOUSE_WANT_MOVE);
       break;
 
+    case 1004:
+      reply = state->mode.report_focus;
+      break;
+
     case 1005:
       reply = state->mouse_protocol == MOUSE_UTF8;
       break;
@@ -888,6 +867,10 @@
       reply = state->mode.alt_screen;
       break;
 
+    case 2004:
+      reply = state->mode.bracketpaste;
+      break;
+
     default:
       vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "?%d;%d$y", num, 0);
       return;
@@ -901,6 +884,7 @@
   VTermState *state = user;
   int leader_byte = 0;
   int intermed_byte = 0;
+  int cancel_phantom = 1;
 
   if(leader && leader[0]) {
     if(leader[1]) // longer than 1 char
@@ -950,6 +934,9 @@
   case 0x40: // ICH - ECMA-48 8.3.64
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->pos.row;
     rect.end_row   = state->pos.row + 1;
     rect.start_col = state->pos.col;
@@ -1093,6 +1080,9 @@
   case 0x4c: // IL - ECMA-48 8.3.67
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->pos.row;
     rect.end_row   = SCROLLREGION_BOTTOM(state);
     rect.start_col = SCROLLREGION_LEFT(state);
@@ -1105,6 +1095,9 @@
   case 0x4d: // DL - ECMA-48 8.3.32
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->pos.row;
     rect.end_row   = SCROLLREGION_BOTTOM(state);
     rect.start_col = SCROLLREGION_LEFT(state);
@@ -1117,6 +1110,9 @@
   case 0x50: // DCH - ECMA-48 8.3.26
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->pos.row;
     rect.end_row   = state->pos.row + 1;
     rect.start_col = state->pos.col;
@@ -1182,6 +1178,24 @@
     state->at_phantom = 0;
     break;
 
+  case 0x62: { // REP - ECMA-48 8.3.103
+    const int row_width = THISROWWIDTH(state);
+    count = CSI_ARG_COUNT(args[0]);
+    col = state->pos.col + count;
+    UBOUND(col, row_width);
+    while (state->pos.col < col) {
+      putglyph(state, state->combine_chars, state->combine_width, state->pos);
+      state->pos.col += state->combine_width;
+    }
+    if (state->pos.col + state->combine_width >= row_width) {
+      if (state->mode.autowrap) {
+        state->at_phantom = 1;
+        cancel_phantom = 0;
+      }
+    }
+    break;
+  }
+
   case 0x63: // DA - ECMA-48 8.3.24
     val = CSI_ARG_OR(args[0], 0);
     if(val == 0)
@@ -1357,7 +1371,7 @@
   case 0x72: // DECSTBM - DEC custom
     state->scrollregion_top = CSI_ARG_OR(args[0], 1) - 1;
     state->scrollregion_bottom = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
-    LBOUND(state->scrollregion_top, -1);
+    LBOUND(state->scrollregion_top, 0);
     UBOUND(state->scrollregion_top, state->rows);
     LBOUND(state->scrollregion_bottom, -1);
     if(state->scrollregion_top == 0 && state->scrollregion_bottom == state->rows)
@@ -1365,13 +1379,19 @@
     else
       UBOUND(state->scrollregion_bottom, state->rows);
 
+    if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+      // Invalid
+      state->scrollregion_top    = 0;
+      state->scrollregion_bottom = -1;
+    }
+
     break;
 
   case 0x73: // DECSLRM - DEC custom
     // Always allow setting these margins, just they won't take effect without DECVSSM
     state->scrollregion_left = CSI_ARG_OR(args[0], 1) - 1;
     state->scrollregion_right = argcount < 2 || CSI_ARG_IS_MISSING(args[1]) ? -1 : CSI_ARG(args[1]);
-    LBOUND(state->scrollregion_left, -1);
+    LBOUND(state->scrollregion_left, 0);
     UBOUND(state->scrollregion_left, state->cols);
     LBOUND(state->scrollregion_right, -1);
     if(state->scrollregion_left == 0 && state->scrollregion_right == state->cols)
@@ -1379,11 +1399,21 @@
     else
       UBOUND(state->scrollregion_right, state->cols);
 
+    if(state->scrollregion_right > -1 &&
+       state->scrollregion_right <= state->scrollregion_left) {
+      // Invalid
+      state->scrollregion_left  = 0;
+      state->scrollregion_right = -1;
+    }
+
     break;
 
   case INTERMED('\'', 0x7D): // DECIC
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->scrollregion_top;
     rect.end_row   = SCROLLREGION_BOTTOM(state);
     rect.start_col = state->pos.col;
@@ -1396,6 +1426,9 @@
   case INTERMED('\'', 0x7E): // DECDC
     count = CSI_ARG_COUNT(args[0]);
 
+    if(!is_cursor_in_scrollregion(state))
+      break;
+
     rect.start_row = state->scrollregion_top;
     rect.end_row   = SCROLLREGION_BOTTOM(state);
     rect.start_col = state->pos.col;
@@ -1406,12 +1439,16 @@
     break;
 
   default:
+    if(state->fallbacks && state->fallbacks->csi)
+      if((*state->fallbacks->csi)(leader, args, argcount, intermed, command, state->fbdata))
+        return 1;
+
     return 0;
   }
 
   if(state->mode.origin) {
     LBOUND(state->pos.row, state->scrollregion_top);
-    UBOUND(state->pos.row, state->scrollregion_bottom-1);
+    UBOUND(state->pos.row, SCROLLREGION_BOTTOM(state)-1);
     LBOUND(state->pos.col, SCROLLREGION_LEFT(state));
     UBOUND(state->pos.col, SCROLLREGION_RIGHT(state)-1);
   }
@@ -1422,7 +1459,28 @@
     UBOUND(state->pos.col, THISROWWIDTH(state)-1);
   }
 
-  updatecursor(state, &oldpos, 1);
+  updatecursor(state, &oldpos, cancel_phantom);
+
+#ifdef DEBUG
+  if(state->pos.row < 0 || state->pos.row >= state->rows ||
+     state->pos.col < 0 || state->pos.col >= state->cols) {
+    fprintf(stderr, "Position out of bounds after CSI %c: (%d,%d)\n",
+        command, state->pos.row, state->pos.col);
+    abort();
+  }
+
+  if(SCROLLREGION_BOTTOM(state) <= state->scrollregion_top) {
+    fprintf(stderr, "Scroll region height out of bounds after CSI %c: %d <= %d\n",
+        command, SCROLLREGION_BOTTOM(state), state->scrollregion_top);
+    abort();
+  }
+
+  if(SCROLLREGION_RIGHT(state) <= SCROLLREGION_LEFT(state)) {
+    fprintf(stderr, "Scroll region width out of bounds after CSI %c: %d <= %d\n",
+        command, SCROLLREGION_RIGHT(state), SCROLLREGION_LEFT(state));
+    abort();
+  }
+#endif
 
   return 1;
 }
@@ -1447,6 +1505,9 @@
     settermprop_string(state, VTERM_PROP_TITLE, command + 2, cmdlen - 2);
     return 1;
   }
+  else if(state->fallbacks && state->fallbacks->osc)
+    if((*state->fallbacks->osc)(command, cmdlen, state->fbdata))
+      return 1;
 
   return 0;
 }
@@ -1508,6 +1569,9 @@
     request_status_string(state, command+2, cmdlen-2);
     return 1;
   }
+  else if(state->fallbacks && state->fallbacks->dcs)
+    if((*state->fallbacks->dcs)(command, cmdlen, state->fbdata))
+      return 1;
 
   return 0;
 }
@@ -1563,6 +1627,11 @@
   state->rows = rows;
   state->cols = cols;
 
+  if(state->scrollregion_bottom > -1)
+    UBOUND(state->scrollregion_bottom, state->rows);
+  if(state->scrollregion_right > -1)
+    UBOUND(state->scrollregion_right, state->cols);
+
   VTermPos delta = { 0, 0 };
 
   if(state->callbacks && state->callbacks->resize)
@@ -1615,7 +1684,7 @@
   if(*state->encoding_utf8.enc->init)
     (*state->encoding_utf8.enc->init)(state->encoding_utf8.enc, state->encoding_utf8.data);
 
-  vterm_set_parser_callbacks(vt, &parser_callbacks, state);
+  vterm_parser_set_callbacks(vt, &parser_callbacks, state);
 
   return state;
 }
@@ -1635,6 +1704,8 @@
   state->mode.alt_screen      = 0;
   state->mode.origin          = 0;
   state->mode.leftrightmargin = 0;
+  state->mode.bracketpaste    = 0;
+  state->mode.report_focus    = 0;
 
   state->vt->mode.ctrl8bit   = 0;
 
@@ -1703,6 +1774,28 @@
   }
 }
 
+void *vterm_state_get_cbdata(VTermState *state)
+{
+  return state->cbdata;
+}
+
+void vterm_state_set_unrecognised_fallbacks(VTermState *state, const VTermParserCallbacks *fallbacks, void *user)
+{
+  if(fallbacks) {
+    state->fallbacks = fallbacks;
+    state->fbdata = user;
+  }
+  else {
+    state->fallbacks = NULL;
+    state->fbdata = NULL;
+  }
+}
+
+void *vterm_state_get_unrecognised_fbdata(VTermState *state)
+{
+  return state->fbdata;
+}
+
 int vterm_state_set_termprop(VTermState *state, VTermProp prop, VTermValue *val)
 {
   /* Only store the new value of the property if usercode said it was happy.
@@ -1740,11 +1833,35 @@
       erase(state, rect, 0);
     }
     return 1;
+  case VTERM_PROP_MOUSE:
+    state->mouse_flags = 0;
+    if(val->number)
+      state->mouse_flags |= MOUSE_WANT_CLICK;
+    if(val->number == VTERM_PROP_MOUSE_DRAG)
+      state->mouse_flags |= MOUSE_WANT_DRAG;
+    if(val->number == VTERM_PROP_MOUSE_MOVE)
+      state->mouse_flags |= MOUSE_WANT_MOVE;
+    return 1;
+
+  case VTERM_N_PROPS:
+    return 0;
   }
 
   return 0;
 }
 
+void vterm_state_focus_in(VTermState *state)
+{
+  if(state->mode.report_focus)
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "I");
+}
+
+void vterm_state_focus_out(VTermState *state)
+{
+  if(state->mode.report_focus)
+    vterm_push_output_sprintf_ctrl(state->vt, C1_CSI, "O");
+}
+
 const VTermLineInfo *vterm_state_get_lineinfo(const VTermState *state, int row)
 {
   return state->lineinfo + row;
diff --git a/src/unicode.c b/src/unicode.c
index 69b7682..0d1b5ff 100644
--- a/src/unicode.c
+++ b/src/unicode.c
@@ -67,8 +67,6 @@
  * Latest version: http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c
  */
 
-#include <wchar.h>
-
 struct interval {
   int first;
   int last;
@@ -129,7 +127,7 @@
 
 
 /* auxiliary function for binary search in interval table */
-static int bisearch(wchar_t ucs, const struct interval *table, int max) {
+static int bisearch(uint32_t ucs, const struct interval *table, int max) {
   int min = 0;
   int mid;
 
@@ -177,12 +175,12 @@
  *      ISO 8859-1 and WGL4 characters, Unicode control characters,
  *      etc.) have a column width of 1.
  *
- * This implementation assumes that wchar_t characters are encoded
+ * This implementation assumes that uint32_t characters are encoded
  * in ISO 10646.
  */
 
 
-static int mk_wcwidth(wchar_t ucs)
+static int mk_wcwidth(uint32_t ucs)
 {
   /* test for 8-bit control characters */
   if (ucs == 0)
@@ -214,7 +212,7 @@
 }
 
 
-static int mk_wcswidth(const wchar_t *pwcs, size_t n)
+static int mk_wcswidth(const uint32_t *pwcs, size_t n)
 {
   int w, width = 0;
 
@@ -237,7 +235,7 @@
  * the traditional terminal character-width behaviour. It is not
  * otherwise recommended for general use.
  */
-static int mk_wcwidth_cjk(wchar_t ucs)
+static int mk_wcwidth_cjk(uint32_t ucs)
 {
   /* sorted list of non-overlapping intervals of East Asian Ambiguous
    * characters, generated by "uniset +WIDTH-A -cat=Me -cat=Mn -cat=Cf c" */
@@ -305,7 +303,7 @@
 }
 
 
-static int mk_wcswidth_cjk(const wchar_t *pwcs, size_t n)
+static int mk_wcswidth_cjk(const uint32_t *pwcs, size_t n)
 {
   int w, width = 0;
 
@@ -321,12 +319,19 @@
 // ################################
 // ### The rest added by Paul Evans
 
-INTERNAL int vterm_unicode_width(int codepoint)
+static const struct interval fullwidth[] = {
+#include "fullwidth.inc"
+};
+
+INTERNAL int vterm_unicode_width(uint32_t codepoint)
 {
+  if(bisearch(codepoint, fullwidth, sizeof(fullwidth) / sizeof(fullwidth[0]) - 1))
+    return 2;
+
   return mk_wcwidth(codepoint);
 }
 
-INTERNAL int vterm_unicode_is_combining(int codepoint)
+INTERNAL int vterm_unicode_is_combining(uint32_t codepoint)
 {
   return bisearch(codepoint, combining, sizeof(combining) / sizeof(struct interval) - 1);
 }
diff --git a/src/vterm.c b/src/vterm.c
index 04651d3..843bb47 100644
--- a/src/vterm.c
+++ b/src/vterm.c
@@ -43,11 +43,14 @@
   vt->rows = rows;
   vt->cols = cols;
 
-  vt->parser_state = NORMAL;
+  vt->parser.state = NORMAL;
 
-  vt->strbuffer_len = 64;
-  vt->strbuffer_cur = 0;
-  vt->strbuffer = vterm_allocator_malloc(vt, vt->strbuffer_len);
+  vt->parser.callbacks = NULL;
+  vt->parser.cbdata    = NULL;
+
+  vt->parser.strbuffer_len = 64;
+  vt->parser.strbuffer_cur = 0;
+  vt->parser.strbuffer = vterm_allocator_malloc(vt, vt->parser.strbuffer_len);
 
   vt->outbuffer_len = 64;
   vt->outbuffer_cur = 0;
@@ -64,7 +67,7 @@
   if(vt->state)
     vterm_state_free(vt->state);
 
-  vterm_allocator_free(vt, vt->strbuffer);
+  vterm_allocator_free(vt, vt->parser.strbuffer);
   vterm_allocator_free(vt, vt->outbuffer);
 
   vterm_allocator_free(vt, vt);
@@ -93,17 +96,16 @@
   vt->rows = rows;
   vt->cols = cols;
 
-  if(vt->parser_callbacks && vt->parser_callbacks->resize)
-    (*vt->parser_callbacks->resize)(rows, cols, vt->cbdata);
+  if(vt->parser.callbacks && vt->parser.callbacks->resize)
+    (*vt->parser.callbacks->resize)(rows, cols, vt->parser.cbdata);
 }
 
-void vterm_set_parser_callbacks(VTerm *vt, const VTermParserCallbacks *callbacks, void *user)
+int vterm_get_utf8(const VTerm *vt)
 {
-  vt->parser_callbacks = callbacks;
-  vt->cbdata = user;
+  return vt->mode.utf8;
 }
 
-void vterm_parser_set_utf8(VTerm *vt, int is_utf8)
+void vterm_set_utf8(VTerm *vt, int is_utf8)
 {
   vt->mode.utf8 = is_utf8;
 }
@@ -111,7 +113,7 @@
 INTERNAL void vterm_push_output_bytes(VTerm *vt, const char *bytes, size_t len)
 {
   if(len > vt->outbuffer_len - vt->outbuffer_cur) {
-    fprintf(stderr, "vterm_push_output(): buffer overflow; truncating output\n");
+    DEBUG_LOG("vterm_push_output(): buffer overflow; truncating output\n");
     len = vt->outbuffer_len - vt->outbuffer_cur;
   }
 
@@ -119,12 +121,28 @@
   vt->outbuffer_cur += len;
 }
 
+static int outbuffer_is_full(VTerm *vt)
+{
+  return vt->outbuffer_cur >= vt->outbuffer_len - 1;
+}
+
 INTERNAL void vterm_push_output_vsprintf(VTerm *vt, const char *format, va_list args)
 {
+  if(outbuffer_is_full(vt)) {
+    DEBUG_LOG("vterm_push_output(): buffer overflow; truncating output\n");
+    return;
+  }
+
   int written = vsnprintf(vt->outbuffer + vt->outbuffer_cur,
       vt->outbuffer_len - vt->outbuffer_cur,
       format, args);
-  vt->outbuffer_cur += written;
+
+  if(written == vt->outbuffer_len - vt->outbuffer_cur) {
+    /* output was truncated */
+    vt->outbuffer_cur = vt->outbuffer_len - 1;
+  }
+  else
+    vt->outbuffer_cur += written;
 }
 
 INTERNAL void vterm_push_output_sprintf(VTerm *vt, const char *format, ...)
@@ -137,8 +155,10 @@
 
 INTERNAL void vterm_push_output_sprintf_ctrl(VTerm *vt, unsigned char ctrl, const char *fmt, ...)
 {
+  size_t orig_cur = vt->outbuffer_cur;
+
   if(ctrl >= 0x80 && !vt->mode.ctrl8bit)
-    vterm_push_output_sprintf(vt, "\e%c", ctrl - 0x40);
+    vterm_push_output_sprintf(vt, ESC_S "%c", ctrl - 0x40);
   else
     vterm_push_output_sprintf(vt, "%c", ctrl);
 
@@ -146,12 +166,17 @@
   va_start(args, fmt);
   vterm_push_output_vsprintf(vt, fmt, args);
   va_end(args);
+
+  if(outbuffer_is_full(vt))
+    vt->outbuffer_cur = orig_cur;
 }
 
 INTERNAL void vterm_push_output_sprintf_dcs(VTerm *vt, const char *fmt, ...)
 {
+  size_t orig_cur = vt->outbuffer_cur;
+
   if(!vt->mode.ctrl8bit)
-    vterm_push_output_sprintf(vt, "\e%c", C1_DCS - 0x40);
+    vterm_push_output_sprintf(vt, ESC_S "%c", C1_DCS - 0x40);
   else
     vterm_push_output_sprintf(vt, "%c", C1_DCS);
 
@@ -161,11 +186,9 @@
   va_end(args);
 
   vterm_push_output_sprintf_ctrl(vt, C1_ST, "");
-}
 
-size_t vterm_output_bufferlen(VTerm *vt)
-{
-  return vterm_output_get_buffer_current(vt);
+  if(outbuffer_is_full(vt))
+    vt->outbuffer_cur = orig_cur;
 }
 
 size_t vterm_output_get_buffer_size(const VTerm *vt)
@@ -183,7 +206,7 @@
   return vt->outbuffer_len - vt->outbuffer_cur;
 }
 
-size_t vterm_output_bufferread(VTerm *vt, char *buffer, size_t len)
+size_t vterm_output_read(VTerm *vt, char *buffer, size_t len)
 {
   if(len > vt->outbuffer_cur)
     len = vt->outbuffer_cur;
@@ -210,6 +233,8 @@
     case VTERM_ATTR_FONT:       return VTERM_VALUETYPE_INT;
     case VTERM_ATTR_FOREGROUND: return VTERM_VALUETYPE_COLOR;
     case VTERM_ATTR_BACKGROUND: return VTERM_VALUETYPE_COLOR;
+
+    case VTERM_N_ATTRS: return 0;
   }
   return 0; /* UNREACHABLE */
 }
@@ -224,6 +249,9 @@
     case VTERM_PROP_ICONNAME:      return VTERM_VALUETYPE_STRING;
     case VTERM_PROP_REVERSE:       return VTERM_VALUETYPE_BOOL;
     case VTERM_PROP_CURSORSHAPE:   return VTERM_VALUETYPE_INT;
+    case VTERM_PROP_MOUSE:         return VTERM_VALUETYPE_INT;
+
+    case VTERM_N_PROPS: return 0;
   }
   return 0; /* UNREACHABLE */
 }
diff --git a/src/vterm_internal.h b/src/vterm_internal.h
index 4bc8e6b..363faee 100644
--- a/src/vterm_internal.h
+++ b/src/vterm_internal.h
@@ -11,6 +11,19 @@
 # define INTERNAL
 #endif
 
+#ifdef DEBUG
+# define DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__)
+#else
+# define DEBUG_LOG(...)
+#endif
+
+#define ESC_S "\x1b"
+
+#define INTERMED_MAX 16
+
+#define CSI_ARGS_MAX 16
+#define CSI_LEADER_MAX 16
+
 typedef struct VTermEncoding VTermEncoding;
 
 typedef struct {
@@ -33,11 +46,6 @@
   unsigned int font:4; /* To store 0-9 */
 };
 
-static inline int vterm_color_equal(VTermColor a, VTermColor b)
-{
-  return a.red == b.red && a.green == b.green && a.blue == b.blue;
-}
-
 struct VTermState
 {
   VTerm *vt;
@@ -45,6 +53,9 @@
   const VTermStateCallbacks *callbacks;
   void *cbdata;
 
+  const VTermParserCallbacks *fallbacks;
+  void *fbdata;
+
   int rows;
   int cols;
 
@@ -72,6 +83,10 @@
   int mouse_col, mouse_row;
   int mouse_buttons;
   int mouse_flags;
+#define MOUSE_WANT_CLICK 0x01
+#define MOUSE_WANT_DRAG  0x02
+#define MOUSE_WANT_MOVE  0x04
+
   enum { MOUSE_X10, MOUSE_UTF8, MOUSE_SGR, MOUSE_RXVT } mouse_protocol;
 
   /* Last glyph output, for Unicode recombining purposes */
@@ -81,18 +96,20 @@
   VTermPos combine_pos;   // Position before movement
 
   struct {
-    int keypad:1;
-    int cursor:1;
-    int autowrap:1;
-    int insert:1;
-    int newline:1;
-    int cursor_visible:1;
-    int cursor_blink:1;
+    unsigned int keypad:1;
+    unsigned int cursor:1;
+    unsigned int autowrap:1;
+    unsigned int insert:1;
+    unsigned int newline:1;
+    unsigned int cursor_visible:1;
+    unsigned int cursor_blink:1;
     unsigned int cursor_shape:2;
-    int alt_screen:1;
-    int origin:1;
-    int screen:1;
-    int leftrightmargin:1;
+    unsigned int alt_screen:1;
+    unsigned int origin:1;
+    unsigned int screen:1;
+    unsigned int leftrightmargin:1;
+    unsigned int bracketpaste:1;
+    unsigned int report_focus:1;
   } mode;
 
   VTermEncodingInstance encoding[4], encoding_utf8;
@@ -104,8 +121,6 @@
   VTermColor default_bg;
   VTermColor colors[16]; // Store the 8 ANSI and the 8 ANSI high-brights only
 
-  int fg_index;
-  int bg_index;
   int bold_is_highbright;
 
   unsigned int protected_cell : 1;
@@ -116,13 +131,20 @@
     struct VTermPen pen;
 
     struct {
-      int cursor_visible:1;
-      int cursor_blink:1;
+      unsigned int cursor_visible:1;
+      unsigned int cursor_blink:1;
       unsigned int cursor_shape:2;
     } mode;
   } saved;
 };
 
+typedef enum {
+  VTERM_PARSER_OSC,
+  VTERM_PARSER_DCS,
+
+  VTERM_N_PARSER_TYPES
+} VTermParserStringType;
+
 struct VTerm
 {
   VTermAllocatorFunctions *allocator;
@@ -132,26 +154,41 @@
   int cols;
 
   struct {
-    int utf8:1;
-    int ctrl8bit:1;
+    unsigned int utf8:1;
+    unsigned int ctrl8bit:1;
   } mode;
 
-  enum VTermParserState {
-    NORMAL,
-    CSI,
-    OSC,
-    DCS,
-    ESC,
-    ESC_IN_OSC,
-    ESC_IN_DCS,
-  } parser_state;
-  const VTermParserCallbacks *parser_callbacks;
-  void *cbdata;
+  struct {
+    enum VTermParserState {
+      NORMAL,
+      CSI_LEADER,
+      CSI_ARGS,
+      CSI_INTERMED,
+      ESC,
+      /* below here are the "string states" */
+      STRING,
+      ESC_IN_STRING,
+    } state;
+
+    int intermedlen;
+    char intermed[INTERMED_MAX];
+
+    int csi_leaderlen;
+    char csi_leader[CSI_LEADER_MAX];
+
+    int csi_argi;
+    long csi_args[CSI_ARGS_MAX];
+
+    const VTermParserCallbacks *callbacks;
+    void *cbdata;
+
+    VTermParserStringType stringtype;
+    char  *strbuffer;
+    size_t strbuffer_len;
+    size_t strbuffer_cur;
+  } parser;
 
   /* len == malloc()ed size; cur == number of valid bytes */
-  char  *strbuffer;
-  size_t strbuffer_len;
-  size_t strbuffer_cur;
 
   char  *outbuffer;
   size_t outbuffer_len;
@@ -203,7 +240,7 @@
 
 VTermEncoding *vterm_lookup_encoding(VTermEncodingType type, char designation);
 
-int vterm_unicode_width(int codepoint);
-int vterm_unicode_is_combining(int codepoint);
+int vterm_unicode_width(uint32_t codepoint);
+int vterm_unicode_is_combining(uint32_t codepoint);
 
 #endif