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"], },