Moterm part 1: Add MotermModel, a "model" for terminal emulation.

This currently wraps libteken, but abstracts this dependency away.

R=erg@chromium.org

Review URL: https://codereview.chromium.org/1130563002
diff --git a/BUILD.gn b/BUILD.gn
index f8f967d..c6d97a5 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -7,6 +7,7 @@
   testonly = true
 
   deps = [
+    "//apps",
     "//crypto:crypto_unittests",
     "//examples",
     "//mojo",
diff --git a/apps/BUILD.gn b/apps/BUILD.gn
new file mode 100644
index 0000000..7602322
--- /dev/null
+++ b/apps/BUILD.gn
@@ -0,0 +1,11 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+group("apps") {
+  testonly = true
+
+  deps = [
+    "//apps/moterm:apptests",
+  ]
+}
diff --git a/apps/moterm/BUILD.gn b/apps/moterm/BUILD.gn
new file mode 100644
index 0000000..2d20419
--- /dev/null
+++ b/apps/moterm/BUILD.gn
@@ -0,0 +1,39 @@
+# Copyright 2015 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("//mojo/public/mojo_application.gni")
+
+source_set("model") {
+  sources = [
+    "moterm_model.cc",
+    "moterm_model.h",
+  ]
+
+  deps = [
+    "//third_party/libteken",
+  ]
+
+  forward_dependent_configs_from = [ "//third_party/libteken" ]
+
+  public_deps = [
+    "//base",
+  ]
+}
+
+mojo_native_application("apptests") {
+  output_name = "moterm_apptests"
+
+  testonly = true
+
+  sources = [
+    "moterm_model_unittest.cc",
+  ]
+
+  deps = [
+    ":model",
+    "//mojo/application",
+    "//mojo/application:test_support",
+    "//testing/gtest",
+  ]
+}
diff --git a/apps/moterm/moterm_model.cc b/apps/moterm/moterm_model.cc
new file mode 100644
index 0000000..3b32158
--- /dev/null
+++ b/apps/moterm/moterm_model.cc
@@ -0,0 +1,364 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "apps/moterm/moterm_model.h"
+
+#include <string.h>
+
+#include <algorithm>
+#include <limits>
+
+#include "base/logging.h"
+
+namespace {
+
+// Moterm -> teken conversions:
+
+teken_pos_t MotermToTekenSize(const MotermModel::Size& size) {
+  DCHECK_LE(size.rows, std::numeric_limits<teken_unit_t>::max());
+  DCHECK_LE(size.columns, std::numeric_limits<teken_unit_t>::max());
+  teken_pos_t rv = {static_cast<teken_unit_t>(size.rows),
+                    static_cast<teken_unit_t>(size.columns)};
+  return rv;
+}
+
+// Teken -> moterm conversions:
+
+MotermModel::Position TekenToMotermPosition(const teken_pos_t& position) {
+  return MotermModel::Position(static_cast<int>(position.tp_row),
+                               static_cast<int>(position.tp_col));
+}
+
+MotermModel::Size TekenToMotermSize(const teken_pos_t& size) {
+  return MotermModel::Size(size.tp_row, size.tp_col);
+}
+
+MotermModel::Rectangle TekenToMotermRectangle(const teken_rect_t& rectangle) {
+  return MotermModel::Rectangle(
+      static_cast<int>(rectangle.tr_begin.tp_row),
+      static_cast<int>(rectangle.tr_begin.tp_col),
+      rectangle.tr_end.tp_row - rectangle.tr_begin.tp_row,
+      rectangle.tr_end.tp_col - rectangle.tr_begin.tp_col);
+}
+
+MotermModel::Color TekenToMotermColor(teken_color_t color, bool bold) {
+  static const uint8_t rgb[TC_NCOLORS][3] = {
+      {0x00, 0x00, 0x00},  // Black.
+      {0x80, 0x00, 0x00},  // Red.
+      {0x00, 0x80, 0x00},  // Green.
+      {0x80, 0x80, 0x00},  // Yellow (even if teken thinks it's brown).
+      {0x00, 0x00, 0x80},  // Blue.
+      {0x80, 0x00, 0x80},  // Magenta.
+      {0x00, 0x80, 0x80},  // Cyan.
+      {0xc0, 0xc0, 0xc0}   // White.
+  };
+  static const uint8_t bold_rgb[TC_NCOLORS][3] = {
+      {0x80, 0x80, 0x80},  // Black.
+      {0xff, 0x00, 0x00},  // Red.
+      {0x00, 0xff, 0x00},  // Green.
+      {0xff, 0xff, 0x00},  // Yellow (even if teken thinks it's brown).
+      {0x00, 0x00, 0xff},  // Blue.
+      {0xff, 0x00, 0xff},  // Magenta.
+      {0x00, 0xff, 0xff},  // Cyan.
+      {0xff, 0xff, 0xff}   // White.
+  };
+  DCHECK_LT(color, static_cast<unsigned>(TC_NCOLORS));
+  return bold ? MotermModel::Color(bold_rgb[color][0], bold_rgb[color][1],
+                                   bold_rgb[color][2])
+              : MotermModel::Color(rgb[color][0], rgb[color][1], rgb[color][2]);
+}
+
+// Utility functions:
+
+MotermModel::Rectangle EnclosingRectangle(const MotermModel::Rectangle& rect1,
+                                          const MotermModel::Rectangle& rect2) {
+  if (rect1.IsEmpty())
+    return rect2;
+  if (rect2.IsEmpty())
+    return rect1;
+
+  int start_row = std::min(rect1.position.row, rect2.position.row);
+  int start_col = std::min(rect1.position.column, rect2.position.column);
+  // TODO(vtl): Some theoretical overflows here.
+  int end_row =
+      std::max(rect1.position.row + static_cast<int>(rect1.size.rows),
+               rect2.position.row + static_cast<int>(rect2.size.rows));
+  int end_col =
+      std::max(rect1.position.column + static_cast<int>(rect1.size.columns),
+               rect2.position.column + static_cast<int>(rect2.size.columns));
+  DCHECK_LE(start_row, end_row);
+  DCHECK_LE(start_col, end_col);
+  return MotermModel::Rectangle(start_row, start_col,
+                                static_cast<unsigned>(end_row - start_row),
+                                static_cast<unsigned>(end_col - start_col));
+}
+
+}  // namespace
+
+const MotermModel::Attributes MotermModel::kAttributesBold;
+const MotermModel::Attributes MotermModel::kAttributesUnderline;
+const MotermModel::Attributes MotermModel::kAttributesBlink;
+
+const unsigned MotermModel::kMaxRows;
+const unsigned MotermModel::kMaxColumns;
+
+MotermModel::MotermModel(const Size& max_size, const Size& size)
+    : max_size_(max_size), terminal_(), current_state_changes_() {
+  DCHECK_GT(max_size_.rows, 0u);
+  DCHECK_LE(max_size_.rows, kMaxRows);
+  DCHECK_GT(max_size_.columns, 0u);
+  DCHECK_LE(max_size_.columns, kMaxColumns);
+
+  DCHECK_GT(size.rows, 0u);
+  DCHECK_LE(size.rows, max_size_.rows);
+  DCHECK_GT(size.columns, 0u);
+  DCHECK_LE(size.columns, max_size_.columns);
+
+  size_t num_chars = max_size_.rows * max_size_.columns;
+  characters_.reset(new teken_char_t[num_chars]);
+  memset(characters_.get(), 0, num_chars * sizeof(characters_[0]));
+  attributes_.reset(new teken_attr_t[num_chars]);
+  memset(attributes_.get(), 0, num_chars * sizeof(attributes_[0]));
+
+  static const teken_funcs_t callbacks = {&MotermModel::OnBellThunk,
+                                          &MotermModel::OnCursorThunk,
+                                          &MotermModel::OnPutcharThunk,
+                                          &MotermModel::OnFillThunk,
+                                          &MotermModel::OnCopyThunk,
+                                          &MotermModel::OnParamThunk,
+                                          &MotermModel::OnRespondThunk};
+  teken_init(&terminal_, &callbacks, this);
+
+  teken_pos_t s = MotermToTekenSize(size);
+  teken_set_winsize(&terminal_, &s);
+}
+
+MotermModel::~MotermModel() {
+}
+
+void MotermModel::ProcessInput(const void* input_bytes,
+                               size_t num_input_bytes,
+                               StateChanges* state_changes) {
+  DCHECK(state_changes);
+  DCHECK(!current_state_changes_);
+  current_state_changes_ = state_changes;
+
+  // Get the initial cursor position, so we'll be able to tell if it moved.
+  teken_pos_t initial_cursor_pos = *teken_get_cursor(&terminal_);
+
+  // Note: This may call some of our callbacks.
+  teken_input(&terminal_, input_bytes, num_input_bytes);
+
+  teken_pos_t final_cursor_pos = *teken_get_cursor(&terminal_);
+  if (initial_cursor_pos.tp_row != final_cursor_pos.tp_row ||
+      initial_cursor_pos.tp_col != final_cursor_pos.tp_col) {
+    state_changes->cursor_moved = true;
+    // Update dirty rect to include old and new cursor positions.
+    current_state_changes_->dirty_rect = EnclosingRectangle(
+        current_state_changes_->dirty_rect,
+        Rectangle(initial_cursor_pos.tp_row, initial_cursor_pos.tp_col, 1, 1));
+    current_state_changes_->dirty_rect = EnclosingRectangle(
+        current_state_changes_->dirty_rect,
+        Rectangle(final_cursor_pos.tp_row, final_cursor_pos.tp_col, 1, 1));
+  }
+
+  current_state_changes_ = nullptr;
+}
+
+MotermModel::Size MotermModel::GetSize() const {
+  // Teken isn't const-correct, sadly.
+  return TekenToMotermSize(
+      *teken_get_winsize(const_cast<teken_t*>(&terminal_)));
+}
+
+MotermModel::Position MotermModel::GetCursorPosition() const {
+  // Teken isn't const-correct, sadly.
+  return TekenToMotermPosition(
+      *teken_get_cursor(const_cast<teken_t*>(&terminal_)));
+}
+
+MotermModel::CharacterInfo MotermModel::GetCharacterInfoAt(
+    const Position& position) const {
+  DCHECK_GE(position.row, 0);
+  DCHECK_LT(position.row, static_cast<int>(GetSize().rows));
+  DCHECK_GE(position.column, 0);
+  DCHECK_LT(position.column, static_cast<int>(GetSize().columns));
+
+  uint32_t ch = characters_[position.row * max_size_.columns + position.column];
+  const teken_attr_t& teken_attr =
+      attributes_[position.row * max_size_.columns + position.column];
+  Color fg = TekenToMotermColor(teken_attr.ta_fgcolor,
+                                (teken_attr.ta_format & TF_BOLD));
+  Color bg = TekenToMotermColor(teken_attr.ta_bgcolor, false);
+  Attributes attr = 0;
+  if ((teken_attr.ta_format & TF_BOLD))
+    attr |= kAttributesBold;
+  if ((teken_attr.ta_format & TF_UNDERLINE))
+    attr |= kAttributesUnderline;
+  if ((teken_attr.ta_format & TF_BLINK))
+    attr |= kAttributesBlink;
+  if ((teken_attr.ta_format & TF_REVERSE))
+    std::swap(fg, bg);
+  return CharacterInfo(ch, attr, fg, bg);
+}
+
+void MotermModel::SetSize(const Size& size, bool reset) {
+  DCHECK_GT(size.rows, 1u);
+  DCHECK_LE(size.rows, max_size_.rows);
+  DCHECK_GT(size.columns, 1u);
+  DCHECK_LE(size.columns, max_size_.columns);
+  teken_pos_t teken_size = {static_cast<teken_unit_t>(size.rows),
+                            static_cast<teken_unit_t>(size.columns)};
+  if (reset) {
+    teken_set_winsize_noreset(&terminal_, &teken_size);
+  } else {
+    // We'll try a bit harder to keep a sensible cursor position.
+    teken_pos_t cursor_pos =
+        *teken_get_cursor(const_cast<teken_t*>(&terminal_));
+    teken_set_winsize(&terminal_, &teken_size);
+    if (cursor_pos.tp_row >= teken_size.tp_row)
+      cursor_pos.tp_row = teken_size.tp_row - 1;
+    if (cursor_pos.tp_col >= teken_size.tp_col)
+      cursor_pos.tp_col = teken_size.tp_col - 1;
+    teken_set_cursor(&terminal_, &cursor_pos);
+  }
+}
+
+void MotermModel::OnBell() {
+  DCHECK(current_state_changes_);
+  current_state_changes_->bell_count++;
+}
+
+void MotermModel::OnCursor(const teken_pos_t* pos) {
+  DCHECK(current_state_changes_);
+  // Don't do anything. We'll just compare initial and final cursor positions.
+}
+
+void MotermModel::OnPutchar(const teken_pos_t* pos,
+                            teken_char_t ch,
+                            const teken_attr_t* attr) {
+  character_at(pos->tp_row, pos->tp_col) = ch;
+  attribute_at(pos->tp_row, pos->tp_col) = *attr;
+
+  // Update dirty rect.
+  DCHECK(current_state_changes_);
+  current_state_changes_->dirty_rect =
+      EnclosingRectangle(current_state_changes_->dirty_rect,
+                         Rectangle(pos->tp_row, pos->tp_col, 1, 1));
+}
+
+void MotermModel::OnFill(const teken_rect_t* rect,
+                         teken_char_t ch,
+                         const teken_attr_t* attr) {
+  for (size_t row = rect->tr_begin.tp_row; row < rect->tr_end.tp_row; row++) {
+    for (size_t col = rect->tr_begin.tp_col; col < rect->tr_end.tp_col; col++) {
+      character_at(row, col) = ch;
+      attribute_at(row, col) = *attr;
+    }
+  }
+
+  // Update dirty rect.
+  DCHECK(current_state_changes_);
+  current_state_changes_->dirty_rect = EnclosingRectangle(
+      current_state_changes_->dirty_rect, TekenToMotermRectangle(*rect));
+}
+
+void MotermModel::OnCopy(const teken_rect_t* rect, const teken_pos_t* pos) {
+  unsigned height = rect->tr_end.tp_row - rect->tr_begin.tp_row;
+  unsigned width = rect->tr_end.tp_col - rect->tr_begin.tp_col;
+
+  // This is really a "move" (like |memmove()|) -- overlaps are likely. Process
+  // the rows depending on which way (vertically) we're moving.
+  if (pos->tp_row <= rect->tr_begin.tp_row) {
+    // Start from the top row.
+    for (unsigned row = 0; row < height; row++) {
+      // Use |memmove()| here, to in case we're not moving vertically.
+      memmove(&character_at(pos->tp_row + row, pos->tp_col),
+              &character_at(rect->tr_begin.tp_row + row, pos->tp_col),
+              width * sizeof(characters_[0]));
+      memmove(&attribute_at(pos->tp_row + row, pos->tp_col),
+              &attribute_at(rect->tr_begin.tp_row + row, pos->tp_col),
+              width * sizeof(attributes_[0]));
+    }
+  } else {
+    // Start from the bottom row.
+    for (unsigned row = height; row > 0;) {
+      row--;
+      // We can use |memcpy()| here.
+      memcpy(&character_at(pos->tp_row + row, pos->tp_col),
+             &character_at(rect->tr_begin.tp_row + row, pos->tp_col),
+             width * sizeof(characters_[0]));
+      memcpy(&attribute_at(pos->tp_row + row, pos->tp_col),
+             &attribute_at(rect->tr_begin.tp_row + row, pos->tp_col),
+             width * sizeof(attributes_[0]));
+    }
+  }
+
+  // Update dirty rect.
+  DCHECK(current_state_changes_);
+  current_state_changes_->dirty_rect = EnclosingRectangle(
+      current_state_changes_->dirty_rect,
+      Rectangle(static_cast<int>(pos->tp_row), static_cast<int>(pos->tp_col),
+                width, height));
+}
+
+void MotermModel::OnParam(int cmd, unsigned val) {
+  // TODO(vtl)
+  NOTIMPLEMENTED();
+}
+
+void MotermModel::OnRespond(const void* buf, size_t size) {
+  // TODO(vtl)
+  NOTIMPLEMENTED();
+}
+
+// static
+void MotermModel::OnBellThunk(void* ctx) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnBell();
+}
+
+// static
+void MotermModel::OnCursorThunk(void* ctx, const teken_pos_t* pos) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnCursor(pos);
+}
+
+// static
+void MotermModel::OnPutcharThunk(void* ctx,
+                                 const teken_pos_t* pos,
+                                 teken_char_t ch,
+                                 const teken_attr_t* attr) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnPutchar(pos, ch, attr);
+}
+
+// static
+void MotermModel::OnFillThunk(void* ctx,
+                              const teken_rect_t* rect,
+                              teken_char_t ch,
+                              const teken_attr_t* attr) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnFill(rect, ch, attr);
+}
+
+// static
+void MotermModel::OnCopyThunk(void* ctx,
+                              const teken_rect_t* rect,
+                              const teken_pos_t* pos) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnCopy(rect, pos);
+}
+
+// static
+void MotermModel::OnParamThunk(void* ctx, int cmd, unsigned val) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnParam(cmd, val);
+}
+
+// static
+void MotermModel::OnRespondThunk(void* ctx, const void* buf, size_t size) {
+  DCHECK(ctx);
+  return static_cast<MotermModel*>(ctx)->OnRespond(buf, size);
+}
diff --git a/apps/moterm/moterm_model.h b/apps/moterm/moterm_model.h
new file mode 100644
index 0000000..e222aa2
--- /dev/null
+++ b/apps/moterm/moterm_model.h
@@ -0,0 +1,178 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// |MotermModel| is a class providing a model for terminal emulation. The basic
+// operations are providing "input" bytes (this is input from the point of view
+// of the terminal; from the point of view of applications, it's output) and
+// determining what character to display at any given position (with what
+// attributes).
+//
+// Note that no termios-style processing of the "input" bytes is done. The
+// "input" bytes should be as seen on the wire by a serial terminal.
+//
+// This class does not handle "output" from the terminal (i.e., its keyboard,
+// and thus "input" to applications).
+//
+// The current implementation is on top of FreeBSD's libteken, though it would
+// be straightforward to replace it with another terminal emulation library (or
+// implement one directly).
+
+#ifndef APPS_MOTERM_MOTERM_MODEL_H_
+#define APPS_MOTERM_MOTERM_MODEL_H_
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "base/macros.h"
+#include "base/memory/scoped_ptr.h"
+#include "third_party/libteken/teken/teken.h"
+
+class MotermModel {
+ public:
+  // Position: zero-based, starting from upper-left. (Quantities are signed to
+  // allow for relative and off-screen positions.)
+  struct Position {
+    Position(int row = 0, int column = 0) : row(row), column(column) {}
+
+    int row;
+    int column;
+  };
+
+  struct Size {
+    Size(unsigned rows = 0, unsigned columns = 0)
+        : rows(rows), columns(columns) {}
+
+    unsigned rows;
+    unsigned columns;
+  };
+
+  struct Rectangle {
+    Rectangle(int row = 0,
+              int column = 0,
+              unsigned rows = 0,
+              unsigned columns = 0)
+        : position(row, column), size(rows, columns) {}
+
+    bool IsEmpty() const { return !size.rows || !size.columns; }
+
+    Position position;
+    Size size;
+  };
+
+  struct Color {
+    Color(uint8_t red = 0, uint8_t green = 0, uint8_t blue = 0)
+        : red(red), green(green), blue(blue) {}
+
+    uint8_t red;
+    uint8_t green;
+    uint8_t blue;
+  };
+
+  using Attributes = uint32_t;
+  static const Attributes kAttributesBold = 1;
+  static const Attributes kAttributesUnderline = 2;
+  static const Attributes kAttributesBlink = 4;
+
+  struct CharacterInfo {
+    CharacterInfo(uint32_t code_point,
+                  Attributes attributes,
+                  const Color& foreground_color,
+                  const Color& background_color)
+        : code_point(code_point),
+          attributes(attributes),
+          foreground_color(foreground_color),
+          background_color(background_color) {}
+
+    uint32_t code_point;  // Unicode, of course.
+    Attributes attributes;
+    Color foreground_color;
+    Color background_color;
+  };
+
+  struct StateChanges {
+    StateChanges() : cursor_moved(false), bell_count(0), dirty_rect() {}
+
+    bool IsDirty() const {
+      return cursor_moved || bell_count > 0 || !dirty_rect.IsEmpty();
+    }
+    void Reset() { *this = StateChanges(); }
+
+    bool cursor_moved;
+    unsigned bell_count;
+    Rectangle dirty_rect;
+  };
+
+  // Maximum number of rows/columns.
+  static const unsigned kMaxRows = 500;  // TODO(vtl): Made up number.
+  static const unsigned kMaxColumns = T_NUMCOL;
+
+  MotermModel(const Size& max_size, const Size& size);
+  ~MotermModel();
+
+  // Process the given input bytes, reporting (additional) state changes to
+  // |*state_changes| (note: this does not "reset" |*state_changes|, so that
+  // state changes can be accumulated across multiple calls).
+  void ProcessInput(const void* input_bytes,
+                    size_t num_input_bytes,
+                    StateChanges* state_changes);
+
+  Size GetSize() const;
+  Position GetCursorPosition() const;
+  CharacterInfo GetCharacterInfoAt(const Position& position) const;
+
+  void SetSize(const Size& size, bool reset);
+
+ private:
+  teken_char_t& character_at(unsigned row, unsigned column) {
+    return characters_[row * max_size_.columns + column];
+  }
+  teken_attr_t& attribute_at(unsigned row, unsigned column) {
+    return attributes_[row * max_size_.columns + column];
+  }
+
+  // libteken callbacks:
+  void OnBell();
+  void OnCursor(const teken_pos_t* pos);
+  void OnPutchar(const teken_pos_t* pos,
+                 teken_char_t ch,
+                 const teken_attr_t* attr);
+  void OnFill(const teken_rect_t* rect,
+              teken_char_t ch,
+              const teken_attr_t* attr);
+  void OnCopy(const teken_rect_t* rect, const teken_pos_t* pos);
+  void OnParam(int cmd, unsigned val);
+  void OnRespond(const void* buf, size_t size);
+
+  // Thunks for libteken callbacks:
+  static void OnBellThunk(void* ctx);
+  static void OnCursorThunk(void* ctx, const teken_pos_t* pos);
+  static void OnPutcharThunk(void* ctx,
+                             const teken_pos_t* pos,
+                             teken_char_t ch,
+                             const teken_attr_t* attr);
+  static void OnFillThunk(void* ctx,
+                          const teken_rect_t* rect,
+                          teken_char_t ch,
+                          const teken_attr_t* attr);
+  static void OnCopyThunk(void* ctx,
+                          const teken_rect_t* rect,
+                          const teken_pos_t* pos);
+  static void OnParamThunk(void* ctx, int cmd, unsigned val);
+  static void OnRespondThunk(void* ctx, const void* buf, size_t size);
+
+  const Size max_size_;
+
+  scoped_ptr<teken_char_t[]> characters_;
+  scoped_ptr<teken_attr_t[]> attributes_;
+
+  teken_t terminal_;
+
+  // Used by the callbacks. ("Usually" null, but must be non-null whenever a
+  // callback may be called -- it'll point to a stack variable.)
+  StateChanges* current_state_changes_;
+
+  DISALLOW_COPY_AND_ASSIGN(MotermModel);
+};
+
+#endif  // APPS_MOTERM_MOTERM_MODEL_H_
diff --git a/apps/moterm/moterm_model_unittest.cc b/apps/moterm/moterm_model_unittest.cc
new file mode 100644
index 0000000..272ffd9
--- /dev/null
+++ b/apps/moterm/moterm_model_unittest.cc
@@ -0,0 +1,203 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "apps/moterm/moterm_model.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+TEST(MotermModelTest, Position) {
+  MotermModel::Position def;
+  EXPECT_EQ(0, def.row);
+  EXPECT_EQ(0, def.column);
+
+  MotermModel::Position pos(12, 34);
+  EXPECT_EQ(12, pos.row);
+  EXPECT_EQ(34, pos.column);
+}
+
+TEST(MotermModelTest, Size) {
+  MotermModel::Size def;
+  EXPECT_EQ(0u, def.rows);
+  EXPECT_EQ(0u, def.columns);
+
+  MotermModel::Size size(12, 34);
+  EXPECT_EQ(12u, size.rows);
+  EXPECT_EQ(34u, size.columns);
+}
+
+TEST(MotermModelTest, Rectangle) {
+  MotermModel::Rectangle def;
+  EXPECT_EQ(0, def.position.row);
+  EXPECT_EQ(0, def.position.column);
+  EXPECT_EQ(0u, def.size.rows);
+  EXPECT_EQ(0u, def.size.columns);
+  EXPECT_TRUE(def.IsEmpty());
+
+  MotermModel::Rectangle rect1(1, 2, 34, 56);
+  EXPECT_EQ(1, rect1.position.row);
+  EXPECT_EQ(2, rect1.position.column);
+  EXPECT_EQ(34u, rect1.size.rows);
+  EXPECT_EQ(56u, rect1.size.columns);
+  EXPECT_FALSE(rect1.IsEmpty());
+
+  MotermModel::Rectangle rect2(1, 2, 0, 0);
+  EXPECT_EQ(1, rect2.position.row);
+  EXPECT_EQ(2, rect2.position.column);
+  EXPECT_EQ(0u, rect2.size.rows);
+  EXPECT_EQ(0u, rect2.size.columns);
+  EXPECT_TRUE(rect2.IsEmpty());
+
+  MotermModel::Rectangle rect3(0, 0, 1, 2);
+  EXPECT_EQ(0, rect3.position.row);
+  EXPECT_EQ(0, rect3.position.column);
+  EXPECT_EQ(1u, rect3.size.rows);
+  EXPECT_EQ(2u, rect3.size.columns);
+  EXPECT_FALSE(rect3.IsEmpty());
+
+  MotermModel::Rectangle rect4(1, 2, 3, 0);
+  EXPECT_EQ(1, rect4.position.row);
+  EXPECT_EQ(2, rect4.position.column);
+  EXPECT_EQ(3u, rect4.size.rows);
+  EXPECT_EQ(0u, rect4.size.columns);
+  EXPECT_TRUE(rect4.IsEmpty());
+
+  MotermModel::Rectangle rect5(1, 2, 0, 3);
+  EXPECT_EQ(1, rect5.position.row);
+  EXPECT_EQ(2, rect5.position.column);
+  EXPECT_EQ(0u, rect5.size.rows);
+  EXPECT_EQ(3u, rect5.size.columns);
+  EXPECT_TRUE(rect5.IsEmpty());
+}
+
+TEST(MotermModelTest, Color) {
+  MotermModel::Color def;
+  EXPECT_EQ(0u, def.red);
+  EXPECT_EQ(0u, def.green);
+  EXPECT_EQ(0u, def.blue);
+
+  MotermModel::Color color(1, 234, 56);
+  EXPECT_EQ(1u, color.red);
+  EXPECT_EQ(234u, color.green);
+  EXPECT_EQ(56u, color.blue);
+}
+
+TEST(MotermModelTest, CharacterInfo) {
+  MotermModel::CharacterInfo char_info(65, MotermModel::kAttributesBold,
+                                       MotermModel::Color(12, 34, 56),
+                                       MotermModel::Color(123, 45, 67));
+  EXPECT_EQ(65u, char_info.code_point);
+  EXPECT_EQ(MotermModel::kAttributesBold, char_info.attributes);
+  EXPECT_EQ(12u, char_info.foreground_color.red);
+  EXPECT_EQ(34u, char_info.foreground_color.green);
+  EXPECT_EQ(56u, char_info.foreground_color.blue);
+  EXPECT_EQ(123u, char_info.background_color.red);
+  EXPECT_EQ(45u, char_info.background_color.green);
+  EXPECT_EQ(67u, char_info.background_color.blue);
+}
+
+TEST(MotermModelTest, StateChanges) {
+  MotermModel::StateChanges state_changes;
+  EXPECT_FALSE(state_changes.cursor_moved);
+  EXPECT_EQ(0u, state_changes.bell_count);
+  EXPECT_TRUE(state_changes.dirty_rect.IsEmpty());
+  EXPECT_FALSE(state_changes.IsDirty());
+  // Should be the same after reset.
+  state_changes.Reset();
+  EXPECT_FALSE(state_changes.cursor_moved);
+  EXPECT_EQ(0u, state_changes.bell_count);
+  EXPECT_TRUE(state_changes.dirty_rect.IsEmpty());
+  EXPECT_FALSE(state_changes.IsDirty());
+
+  state_changes.cursor_moved = true;
+  EXPECT_TRUE(state_changes.IsDirty());
+  state_changes.Reset();
+  EXPECT_FALSE(state_changes.cursor_moved);
+  EXPECT_FALSE(state_changes.IsDirty());
+
+  state_changes.bell_count++;
+  EXPECT_TRUE(state_changes.IsDirty());
+  state_changes.Reset();
+  EXPECT_EQ(0u, state_changes.bell_count);
+  EXPECT_FALSE(state_changes.IsDirty());
+
+  state_changes.dirty_rect = MotermModel::Rectangle(1, 2, 34, 56);
+  EXPECT_TRUE(state_changes.IsDirty());
+  state_changes.Reset();
+  EXPECT_TRUE(state_changes.dirty_rect.IsEmpty());
+  EXPECT_FALSE(state_changes.IsDirty());
+}
+
+TEST(MotermModelTest, Basic) {
+  MotermModel model(MotermModel::Size(43, 132), MotermModel::Size(25, 80));
+
+  MotermModel::Size size = model.GetSize();
+  EXPECT_EQ(25u, size.rows);
+  EXPECT_EQ(80u, size.columns);
+
+  // The cursor should start out at the upper-left.
+  MotermModel::Position cursor_pos = model.GetCursorPosition();
+  EXPECT_EQ(0, cursor_pos.row);
+  EXPECT_EQ(0, cursor_pos.column);
+
+  MotermModel::StateChanges state_changes;
+  EXPECT_FALSE(state_changes.IsDirty());
+
+  // Print "XYZ" in bright (bold) green on red.
+  static const char kXYZ[] = "\x1b[1;32;41mXYZ";
+  model.ProcessInput(kXYZ, sizeof(kXYZ) - 1, &state_changes);
+  EXPECT_TRUE(state_changes.IsDirty());
+  EXPECT_TRUE(state_changes.cursor_moved);
+  EXPECT_EQ(0u, state_changes.bell_count);
+  EXPECT_FALSE(state_changes.dirty_rect.IsEmpty());
+  // The model has some flexibility in the size of the dirty rectangle (it may
+  // over-report), but it should contain the actually-dirty part.
+  EXPECT_LE(state_changes.dirty_rect.position.row, 0);
+  EXPECT_LE(state_changes.dirty_rect.position.column, 0);
+  EXPECT_GE(state_changes.dirty_rect.size.rows, 1u);
+  EXPECT_GE(state_changes.dirty_rect.size.columns, 3u);
+
+  // Get the 'Y'.
+  MotermModel::CharacterInfo char_info =
+      model.GetCharacterInfoAt(MotermModel::Position(0, 1));
+  EXPECT_EQ(static_cast<uint32_t>('Y'), char_info.code_point);
+  EXPECT_EQ(MotermModel::kAttributesBold, char_info.attributes);
+  // The foreground should be (bright) green-ish; this is a guess at what that
+  // means.
+  EXPECT_GE(char_info.foreground_color.green, 100u);
+  EXPECT_GE(char_info.foreground_color.green / 2,
+            char_info.foreground_color.red);
+  EXPECT_GE(char_info.foreground_color.green / 2,
+            char_info.foreground_color.blue);
+  // The background_color should be (non-bright) red-ish.
+  EXPECT_GE(char_info.background_color.red, 50u);
+  EXPECT_GE(char_info.background_color.red / 2,
+            char_info.background_color.green);
+  EXPECT_GE(char_info.background_color.red / 2,
+            char_info.background_color.blue);
+
+  state_changes.Reset();
+  EXPECT_FALSE(state_changes.IsDirty());
+
+  // Now ring the bell three times.
+  static const char kBellBellBell[] = "\a\a\a";
+  model.ProcessInput(kBellBellBell, sizeof(kBellBellBell) - 1, &state_changes);
+  EXPECT_TRUE(state_changes.IsDirty());
+  EXPECT_FALSE(state_changes.cursor_moved);
+  EXPECT_EQ(3u, state_changes.bell_count);
+  EXPECT_TRUE(state_changes.dirty_rect.IsEmpty());
+
+  model.SetSize(MotermModel::Size(43, 132), false);
+  size = model.GetSize();
+  EXPECT_EQ(43u, size.rows);
+  EXPECT_EQ(132u, size.columns);
+
+  model.SetSize(MotermModel::Size(40, 100), true);
+  size = model.GetSize();
+  EXPECT_EQ(40u, size.rows);
+  EXPECT_EQ(100u, size.columns);
+}
+
+}  // namespace
diff --git a/mojo/tools/data/apptests b/mojo/tools/data/apptests
index 41f6da9..27abd57 100644
--- a/mojo/tools/data/apptests
+++ b/mojo/tools/data/apptests
@@ -50,6 +50,9 @@
     "test": "mojo:mojio_apptests",
   },
   {
+    "test": "mojo:moterm_apptests",
+  },
+  {
     "test": "mojo:mojo_view_manager_client_apptests",
     "shell-args": ["--args-for=mojo:native_viewport_service --use-headless-config --use-osmesa"],
   },