xuty 5 жил өмнө
commit
263e378307
55 өөрчлөгдсөн 6682 нэмэгдсэн , 0 устгасан
  1. 75 0
      .gitignore
  2. 10 0
      .metadata
  3. 3 0
      CHANGELOG.md
  4. 21 0
      LICENSE
  5. 45 0
      README.md
  6. 455 0
      lib/buffer/buffer.dart
  7. 48 0
      lib/buffer/buffer_line.dart
  8. 36 0
      lib/buffer/cell.dart
  9. 90 0
      lib/buffer/cell_attr.dart
  10. 12 0
      lib/buffer/cell_color.dart
  11. 24 0
      lib/color/color_default.dart
  12. 48 0
      lib/color/color_scheme.dart
  13. 30 0
      lib/frontend/char_size.dart
  14. 23 0
      lib/frontend/helpers.dart
  15. 131 0
      lib/frontend/input_listener.dart
  16. 287 0
      lib/frontend/input_map.dart
  17. 25 0
      lib/frontend/mouse_listener.dart
  18. 33 0
      lib/frontend/oscillator.dart
  19. 509 0
      lib/frontend/terminal_view.dart
  20. 27 0
      lib/frontend/text_layout_cache.dart
  21. 835 0
      lib/input/keys.dart
  22. 38 0
      lib/input/keytab/keytab.dart
  23. 200 0
      lib/input/keytab/keytab_default.dart
  24. 21 0
      lib/input/keytab/keytab_escape.dart
  25. 181 0
      lib/input/keytab/keytab_parse.dart
  26. 121 0
      lib/input/keytab/keytab_record.dart
  27. 179 0
      lib/input/keytab/keytab_token.dart
  28. 476 0
      lib/input/keytab/qt_keyname.dart
  29. 1 0
      lib/input/shortcut.dart
  30. 16 0
      lib/mouse/mouse_kind.dart
  31. 58 0
      lib/mouse/mouse_mode.dart
  32. 43 0
      lib/mouse/position.dart
  33. 58 0
      lib/mouse/selection.dart
  34. 84 0
      lib/terminal/ansi.dart
  35. 95 0
      lib/terminal/charset.dart
  36. 401 0
      lib/terminal/csi.dart
  37. 170 0
      lib/terminal/modes.dart
  38. 65 0
      lib/terminal/osc.dart
  39. 50 0
      lib/terminal/sbc.dart
  40. 342 0
      lib/terminal/sgr.dart
  41. 28 0
      lib/terminal/tabs.dart
  42. 427 0
      lib/terminal/terminal.dart
  43. 25 0
      lib/utli/ansi_color.dart
  44. 66 0
      lib/utli/debug_handler.dart
  45. 19 0
      lib/utli/observable.dart
  46. 6 0
      lib/utli/scroll_range.dart
  47. 511 0
      lib/utli/unicode_v11.dart
  48. 4 0
      lib/xterm.dart
  49. BIN
      media/demo-dialog.png
  50. BIN
      media/demo-htop.png
  51. BIN
      media/demo-shell.png
  52. BIN
      media/demo-vim.png
  53. 161 0
      pubspec.lock
  54. 56 0
      pubspec.yaml
  55. 13 0
      test/xterm_test.dart

+ 75 - 0
.gitignore

@@ -0,0 +1,75 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+build/
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

+ 10 - 0
.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: a5fa083906fcaf88b039a717c6e78b9814f3a77c
+  channel: master
+
+project_type: package

+ 3 - 0
CHANGELOG.md

@@ -0,0 +1,3 @@
+## [0.0.1] - 2020-8-1
+
+* First version

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 xuty
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 45 - 0
README.md

@@ -0,0 +1,45 @@
+**xterm.dart**
+
+<p>
+    <img alt="GitHub repo size" src="https://img.shields.io/github/repo-size/TerminalStudio/xterm.dart">
+    <img alt="GitHub issues" src="https://img.shields.io/github/issues-raw/TerminalStudio/xterm.dart">
+    <img alt="GitHub pull requests" src="https://img.shields.io/github/issues-pr/TerminalStudio/xterm.dart">
+</p>
+
+
+Xterm.dart is a fast and fully-featured terminal emulator for Flutter, with support for mobile and desktop platforms.
+
+## Screenshots
+
+<table>
+  <tr>
+    <td>
+		<img width="200px" src="https://raw.githubusercontent.com/TerminalStudio/xterm.dart/master/media/demo-shell.png">
+    </td>
+    <td>
+       <img width="200px" src="https://raw.githubusercontent.com/TerminalStudio/xterm.dart/master/media/demo-vim.png">
+    </td>
+  <tr>
+  </tr>
+    <td>
+       <img width="200px" src="https://raw.githubusercontent.com/TerminalStudio/xterm.dart/master/media/demo-htop.png">
+    </td>
+    <td>
+       <img width="200px" src="https://raw.githubusercontent.com/TerminalStudio/xterm.dart/master/media/demo-dialog.png">
+    </td>
+  </tr>
+</table>
+
+## Features
+
+- 📦 **Works out of the box** No special configuration required.
+- 🚀 **Fast** Renders at 60fps.
+- 😀 **Wide character support** Supports CJK and emojis.
+- ✂️ **Customizable** 
+- ✔ **Frontend independent**: The terminal core can work without flutter frontend.
+
+## Getting Started
+
+## License
+
+This project is licensed under an MIT license.

+ 455 - 0
lib/buffer/buffer.dart

@@ -0,0 +1,455 @@
+import 'dart:math' show max, min;
+
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/cell.dart';
+import 'package:xterm/buffer/cell_attr.dart';
+import 'package:xterm/terminal/charset.dart';
+import 'package:xterm/terminal/terminal.dart';
+import 'package:xterm/utli/scroll_range.dart';
+import 'package:xterm/utli/unicode_v11.dart';
+
+class Buffer {
+  Buffer(this.terminal) {
+    resetVerticalMargins();
+  }
+
+  final Terminal terminal;
+  final lines = <BufferLine>[];
+  final charset = Charset();
+
+  int _cursorX = 0;
+  int _cursorY = 0;
+  int _savedCursorX;
+  int _savedCursorY;
+  int _scrollLinesFromBottom = 0;
+  int _marginTop;
+  int _marginBottom;
+  CellAttr _savedCellAttr;
+
+  int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
+  int get cursorY => _cursorY;
+  int get marginTop => _marginTop;
+  int get marginBottom => _marginBottom;
+
+  void write(String text) {
+    for (var char in text.runes) {
+      writeChar(char);
+    }
+  }
+
+  void writeChar(int codePoint) {
+    codePoint = charset.translate(codePoint);
+
+    final cellWidth = unicodeV11.wcwidth(codePoint);
+    if (_cursorX >= terminal.viewWidth) {
+      newLine();
+      setCursorX(0);
+    }
+
+    final line = currentLine;
+    while (line.length <= _cursorX) {
+      line.add(Cell());
+    }
+
+    final cell = line.getCell(_cursorX);
+    cell.setCodePoint(codePoint);
+    cell.setWidth(cellWidth);
+    cell.setAttr(terminal.cellAttr.copy());
+
+    if (_cursorX < terminal.viewWidth) {
+      _cursorX++;
+    }
+
+    if (cellWidth == 2) {
+      writeChar(0);
+    }
+  }
+
+  BufferLine getViewLine(int index) {
+    if (index > terminal.viewHeight) {
+      return lines.last;
+    }
+
+    while (index >= lines.length) {
+      final newLine = BufferLine();
+      lines.add(newLine);
+    }
+
+    return lines[convertViewLineToRawLine(index)];
+  }
+
+  BufferLine get currentLine {
+    return getViewLine(_cursorY);
+  }
+
+  int get height {
+    return lines.length;
+  }
+
+  int convertViewLineToRawLine(int viewLine) {
+    if (terminal.viewHeight > height) {
+      return viewLine;
+    }
+
+    return viewLine + (height - terminal.viewHeight);
+  }
+
+  int convertRawLineToViewLine(int rawLine) {
+    if (terminal.viewHeight > height) {
+      return rawLine;
+    }
+
+    return rawLine - (height - terminal.viewHeight);
+  }
+
+  void newLine() {
+    if (terminal.lineFeed == false) {
+      setCursorX(0);
+    }
+
+    index();
+  }
+
+  void carriageReturn() {
+    setCursorX(0);
+  }
+
+  void backspace() {
+    if (_cursorX == 0 && currentLine.isWrapped) {
+      movePosition(terminal.viewWidth - 1, -1);
+    } else if (_cursorX == terminal.viewWidth) {
+      movePosition(-2, 0);
+    } else {
+      movePosition(-1, 0);
+    }
+  }
+
+  List<BufferLine> getVisibleLines() {
+    final result = <BufferLine>[];
+
+    for (var i = height - terminal.viewHeight; i < height; i++) {
+      final y = i - scrollOffset;
+      if (y >= 0 && y < height) {
+        result.add(lines[y]);
+      }
+    }
+
+    return result;
+  }
+
+  void eraseDisplayFromCursor() {
+    eraseLineFromCursor();
+
+    for (var i = _cursorY + 1; i < terminal.viewHeight; i++) {
+      getViewLine(i).erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+    }
+  }
+
+  void eraseDisplayToCursor() {
+    eraseLineToCursor();
+
+    for (var i = 0; i < _cursorY; i++) {
+      getViewLine(i).erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+    }
+  }
+
+  void eraseDisplay() {
+    for (var i = 0; i < terminal.viewHeight; i++) {
+      final line = getViewLine(i);
+      line.erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+    }
+  }
+
+  void eraseLineFromCursor() {
+    currentLine.erase(terminal.cellAttr.copy(), _cursorX, terminal.viewWidth);
+  }
+
+  void eraseLineToCursor() {
+    currentLine.erase(terminal.cellAttr.copy(), 0, _cursorX);
+  }
+
+  void eraseLine() {
+    currentLine.erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+  }
+
+  void eraseCharacters(int count) {
+    final start = _cursorX;
+    for (var i = start; i < start + count; i++) {
+      if (i >= currentLine.length) {
+        currentLine.add(Cell(attr: terminal.cellAttr.copy()));
+      } else {
+        currentLine.getCell(i).erase(terminal.cellAttr.copy());
+      }
+    }
+  }
+
+  ScrollRange getAreaScrollRange() {
+    var top = convertViewLineToRawLine(_marginTop);
+    var bottom = convertViewLineToRawLine(_marginBottom) + 1;
+    if (bottom > lines.length) {
+      bottom = lines.length;
+    }
+    return ScrollRange(top, bottom);
+  }
+
+  void areaScrollDown(int lines) {
+    final scrollRange = getAreaScrollRange();
+
+    for (var i = scrollRange.bottom; i > scrollRange.top;) {
+      i--;
+      if (i >= scrollRange.top + lines) {
+        this.lines[i] = this.lines[i - lines];
+      } else {
+        this.lines[i] = BufferLine();
+      }
+    }
+  }
+
+  void areaScrollUp(int lines) {
+    final scrollRange = getAreaScrollRange();
+
+    for (var i = scrollRange.top; i < scrollRange.bottom; i++) {
+      if (i + lines < scrollRange.bottom) {
+        this.lines[i] = this.lines[i + lines];
+      } else {
+        this.lines[i] = BufferLine();
+      }
+    }
+  }
+
+  /// https://vt100.net/docs/vt100-ug/chapter3.html#IND
+  void index() {
+    if (isInScrollableRegion) {
+      if (_cursorY < _marginBottom) {
+        moveCursorY(1);
+      } else {
+        areaScrollUp(1);
+      }
+      return;
+    }
+
+    if (_cursorY >= terminal.viewHeight - 1) {
+      lines.add(BufferLine());
+      if (terminal.maxLines != null && lines.length > terminal.maxLines) {
+        lines.removeRange(0, lines.length - terminal.maxLines);
+      }
+    } else {
+      moveCursorY(1);
+    }
+  }
+
+  /// https://vt100.net/docs/vt100-ug/chapter3.html#RI
+  void reverseIndex() {
+    if (_cursorY == _marginTop) {
+      areaScrollDown(1);
+    } else if (_cursorY > 0) {
+      moveCursorY(-1);
+    }
+  }
+
+  Cell getCell(int col, int row) {
+    final rawRow = convertViewLineToRawLine(row);
+    return getRawCell(col, rawRow);
+  }
+
+  Cell getRawCell(int col, int rawRow) {
+    if (col < 0 || rawRow < 0 || rawRow >= lines.length) {
+      return null;
+    }
+
+    final line = lines[rawRow];
+    if (col >= line.length) {
+      return null;
+    }
+
+    return line.getCell(col);
+  }
+
+  Cell getCellUnderCursor() {
+    return getCell(cursorX, cursorY);
+  }
+
+  void cursorGoForward() {
+    setCursorX(_cursorX + 1);
+    terminal.refresh();
+  }
+
+  void setCursorX(int cursorX) {
+    _cursorX = cursorX.clamp(0, terminal.viewWidth - 1);
+    terminal.refresh();
+  }
+
+  void setCursorY(int cursorY) {
+    _cursorY = cursorY.clamp(0, terminal.viewHeight - 1);
+    terminal.refresh();
+  }
+
+  void moveCursorX(int offset) {
+    setCursorX(_cursorX + offset);
+  }
+
+  void moveCursorY(int offset) {
+    setCursorY(_cursorY + offset);
+  }
+
+  void setPosition(int cursorX, int cursorY) {
+    var maxLine = terminal.viewHeight - 1;
+
+    if (terminal.originMode) {
+      cursorY += _marginTop;
+      maxLine = _marginBottom;
+    }
+
+    _cursorX = cursorX.clamp(0, terminal.viewWidth - 1);
+    _cursorY = cursorY.clamp(0, maxLine);
+  }
+
+  void movePosition(int offsetX, int offsetY) {
+    final cursorX = _cursorX + offsetX;
+    final cursorY = _cursorY + offsetY;
+    setPosition(cursorX, cursorY);
+  }
+
+  int get scrollOffset {
+    return _scrollLinesFromBottom;
+  }
+
+  void setScrollOffset(int offset) {
+    if (height < terminal.viewHeight) return;
+    final maxOffset = height - terminal.viewHeight;
+    _scrollLinesFromBottom = offset.clamp(0, maxOffset);
+    terminal.refresh();
+  }
+
+  void screenScrollUp(int lines) {
+    setScrollOffset(scrollOffset + lines);
+  }
+
+  void screenScrollDown(int lines) {
+    setScrollOffset(scrollOffset - lines);
+  }
+
+  void saveCursor() {
+    _savedCellAttr = terminal.cellAttr.copy();
+    _savedCursorX = _cursorX;
+    _savedCursorY = _cursorY;
+    charset.save();
+  }
+
+  void restoreCursor() {
+    if (_savedCellAttr != null) {
+      terminal.cellAttr = _savedCellAttr.copy();
+    }
+
+    if (_savedCursorX != null) {
+      _cursorX = _savedCursorX;
+    }
+
+    if (_savedCursorY != null) {
+      _cursorY = _savedCursorY;
+    }
+
+    charset.restore();
+  }
+
+  void setVerticalMargins(int top, int bottom) {
+    _marginTop = top.clamp(0, terminal.viewHeight - 1);
+    _marginBottom = bottom.clamp(0, terminal.viewHeight - 1);
+
+    _marginTop = min(_marginTop, _marginBottom);
+    _marginBottom = max(_marginTop, _marginBottom);
+  }
+
+  bool get hasScrollableRegion {
+    return _marginTop > 0 || _marginBottom < (terminal.viewHeight - 1);
+  }
+
+  bool get isInScrollableRegion {
+    return hasScrollableRegion &&
+        _cursorY >= _marginTop &&
+        _cursorY <= _marginBottom;
+  }
+
+  void resetVerticalMargins() {
+    setVerticalMargins(0, terminal.viewHeight - 1);
+  }
+
+  void deleteChars(int count) {
+    final start = _cursorX.clamp(0, currentLine.length);
+    final end = min(_cursorX + count, currentLine.length);
+    currentLine.removeRange(start, end);
+  }
+
+  void clearScrollback() {
+    if (lines.length <= terminal.viewHeight) {
+      return;
+    }
+
+    lines.removeRange(0, lines.length - terminal.viewHeight);
+  }
+
+  void clear() {
+    lines.clear();
+  }
+
+  void insertBlankCharacters(int count) {
+    for (var i = 0; i < count; i++) {
+      final cell = Cell(attr: terminal.cellAttr.copy());
+      currentLine.insert(_cursorX + i, cell);
+    }
+  }
+
+  void insertLines(int count) {
+    if (hasScrollableRegion && !isInScrollableRegion) {
+      return;
+    }
+
+    setCursorX(0);
+
+    for (var i = 0; i < count; i++) {
+      insertLine();
+    }
+  }
+
+  void insertLine() {
+    if (!isInScrollableRegion) {
+      final index = convertViewLineToRawLine(_cursorX);
+      final newLine = BufferLine();
+      lines.insert(index, newLine);
+
+      if (lines.length > terminal.maxLines) {
+        lines.removeRange(0, lines.length - terminal.maxLines);
+      }
+    } else {
+      final bottom = convertViewLineToRawLine(marginBottom);
+
+      final movedLines = lines.getRange(_cursorY, bottom - 1);
+      lines.setRange(_cursorY + 1, bottom, movedLines);
+
+      final newLine = BufferLine();
+      lines[_cursorY] = newLine;
+    }
+  }
+
+  void deleteLines(int count) {
+    if (hasScrollableRegion && !isInScrollableRegion) {
+      return;
+    }
+
+    setCursorX(0);
+
+    for (var i = 0; i < count; i++) {
+      deleteLine();
+    }
+  }
+
+  void deleteLine() {
+    final index = convertViewLineToRawLine(_cursorX);
+
+    if (index >= height) {
+      return;
+    }
+
+    lines.removeAt(index);
+  }
+}

+ 48 - 0
lib/buffer/buffer_line.dart

@@ -0,0 +1,48 @@
+import 'package:xterm/buffer/cell.dart';
+import 'package:xterm/buffer/cell_attr.dart';
+
+class BufferLine {
+  final _cells = <Cell>[];
+  bool _isWrapped = false;
+
+  bool get isWrapped {
+    return _isWrapped;
+  }
+
+  int get length {
+    return _cells.length;
+  }
+
+  void add(Cell cell) {
+    _cells.add(cell);
+  }
+
+  void insert(int index, Cell cell) {
+    _cells.insert(index, cell);
+  }
+
+  void clear() {
+    _cells.clear();
+  }
+
+  void erase(CellAttr attr, int start, int end) {
+    for (var i = start; i < end; i++) {
+      if (i >= length) {
+        add(Cell(attr: attr));
+      } else {
+        getCell(i).erase(attr);
+      }
+    }
+  }
+
+  Cell getCell(int index) {
+    return _cells[index];
+  }
+
+  void removeRange(int start, [int end]) {
+    start = start.clamp(0, _cells.length);
+    end ??= _cells.length;
+    end = end.clamp(start, _cells.length);
+    _cells.removeRange(start, end);
+  }
+}

+ 36 - 0
lib/buffer/cell.dart

@@ -0,0 +1,36 @@
+import 'package:xterm/buffer/cell_attr.dart';
+
+class Cell {
+  Cell({this.codePoint, this.width = 1, this.attr});
+
+  int codePoint;
+  int width;
+  CellAttr attr;
+
+  void setCodePoint(int codePoint) {
+    this.codePoint = codePoint;
+  }
+
+  void setAttr(CellAttr attr) {
+    this.attr = attr;
+  }
+
+  void setWidth(int width) {
+    this.width = width;
+  }
+
+  void reset(CellAttr attr) {
+    codePoint = null;
+    this.attr = attr;
+  }
+
+  void erase(CellAttr attr) {
+    codePoint = null;
+    this.attr = attr;
+  }
+
+  @override
+  String toString() {
+    return 'Cell($codePoint)';
+  }
+}

+ 90 - 0
lib/buffer/cell_attr.dart

@@ -0,0 +1,90 @@
+import 'package:meta/meta.dart';
+import 'package:xterm/buffer/cell_color.dart';
+
+class CellAttr {
+  CellAttr({
+    @required this.fgColor,
+    this.bgColor,
+    this.bold = false,
+    this.faint = false,
+    this.italic = false,
+    this.underline = false,
+    this.blink = false,
+    this.inverse = false,
+    this.invisible = false,
+  });
+
+  CellColor fgColor;
+  CellColor bgColor;
+  bool bold;
+  bool faint;
+  bool italic;
+  bool underline;
+  bool blink;
+  bool inverse;
+  bool invisible;
+
+  CellAttr copy() {
+    return CellAttr(
+      fgColor: this.fgColor,
+      bgColor: this.bgColor,
+      bold: this.bold,
+      faint: this.faint,
+      italic: this.italic,
+      underline: this.underline,
+      blink: this.blink,
+      inverse: this.inverse,
+      invisible: this.invisible,
+    );
+  }
+
+  void reset({
+    @required fgColor,
+    bgColor,
+    bold = false,
+    faint = false,
+    italic = false,
+    underline = false,
+    blink = false,
+    inverse = false,
+    invisible = false,
+  }) {
+    this.fgColor = fgColor;
+    this.bgColor = bgColor;
+    this.bold = bold;
+    this.faint = faint;
+    this.italic = italic;
+    this.underline = underline;
+    this.blink = blink;
+    this.inverse = inverse;
+    this.invisible = invisible;
+  }
+
+  // CellAttr copyWith({
+  //   CellColor fgColour,
+  //   CellColor bgColour,
+  //   bool bold,
+  //   bool faint,
+  //   bool italic,
+  //   bool underline,
+  //   bool blink,
+  //   bool inverse,
+  //   bool invisible,
+  // }) {
+  //   return CellAttr(
+  //     fgColour: fgColour ?? this.fgColour,
+  //     bgColour: bgColour ?? this.bgColour,
+  //     bold: bold ?? this.bold,
+  //     faint: faint ?? this.faint,
+  //     italic: italic ?? this.italic,
+  //     underline: underline ?? this.underline,
+  //     blink: blink ?? this.blink,
+  //     inverse: inverse ?? this.inverse,
+  //     invisible: invisible ?? this.invisible,
+  //   );
+  // }
+}
+
+// class CellAttrTemplate {
+
+// }

+ 12 - 0
lib/buffer/cell_color.dart

@@ -0,0 +1,12 @@
+class CellColor {
+  const CellColor(this.value);
+  const CellColor.empty() : value = 0xFF000000;
+  const CellColor.fromARGB(int a, int r, int g, int b)
+      : value = (((a & 0xff) << 24) |
+                ((r & 0xff) << 16) |
+                ((g & 0xff) << 8) |
+                ((b & 0xff) << 0)) &
+            0xFFFFFFFF;
+
+  final int value;
+}

+ 24 - 0
lib/color/color_default.dart

@@ -0,0 +1,24 @@
+import 'package:xterm/buffer/cell_color.dart';
+import 'package:xterm/color/color_scheme.dart';
+
+final defaultColorScheme = TerminalColourScheme(
+  cursor: CellColor(0xffaeafad),
+  selection: CellColor(0xffffff40),
+  foreground: CellColor(0xffcccccc),
+  background: CellColor(0xff1e1e1e),
+  black: CellColor(0xff000000),
+  white: CellColor(0xffe5e5e5),
+  red: CellColor(0xffcd3131),
+  green: CellColor(0xff0dbc79),
+  yellow: CellColor(0xffe5e510),
+  blue: CellColor(0xff2472c8),
+  magenta: CellColor(0xffbc3fbc),
+  cyan: CellColor(0xff11a8cd),
+  brightBlack: CellColor(0xff666666),
+  brightRed: CellColor(0xfff14c4c),
+  brightGreen: CellColor(0xff23d18b),
+  brightYellow: CellColor(0xfff5f543),
+  brightBlue: CellColor(0xff3b8eea),
+  brightMagenta: CellColor(0xffd670d6),
+  brightCyan: CellColor(0xff29b8db),
+);

+ 48 - 0
lib/color/color_scheme.dart

@@ -0,0 +1,48 @@
+import 'package:meta/meta.dart';
+import 'package:xterm/buffer/cell_color.dart';
+
+class TerminalColourScheme {
+  TerminalColourScheme({
+    @required this.cursor,
+    @required this.selection,
+    @required this.foreground,
+    @required this.background,
+    @required this.black,
+    @required this.white,
+    @required this.red,
+    @required this.green,
+    @required this.yellow,
+    @required this.blue,
+    @required this.magenta,
+    @required this.cyan,
+    @required this.brightBlack,
+    @required this.brightRed,
+    @required this.brightGreen,
+    @required this.brightYellow,
+    @required this.brightBlue,
+    @required this.brightMagenta,
+    @required this.brightCyan,
+  });
+
+  CellColor cursor;
+  CellColor selection;
+
+  CellColor foreground;
+  CellColor background;
+  CellColor black;
+  CellColor red;
+  CellColor green;
+  CellColor yellow;
+  CellColor blue;
+  CellColor magenta;
+  CellColor cyan;
+
+  CellColor brightBlack;
+  CellColor brightRed;
+  CellColor brightGreen;
+  CellColor brightYellow;
+  CellColor brightBlue;
+  CellColor brightMagenta;
+  CellColor brightCyan;
+  CellColor white;
+}

+ 30 - 0
lib/frontend/char_size.dart

@@ -0,0 +1,30 @@
+class CharSize {
+  CharSize({
+    this.width,
+    this.height,
+    this.letterSpacing,
+    this.lineSpacing,
+    this.effectWidth,
+    this.effectHeight,
+  });
+
+  final double width;
+  final double height;
+  final double letterSpacing;
+  final double lineSpacing;
+  final double effectWidth;
+  final double effectHeight;
+
+  @override
+  String toString() {
+    final data = {
+      'width': width,
+      'height': height,
+      'letterSpacing': letterSpacing,
+      'lineSpacing': lineSpacing,
+      'effectWidth': effectWidth,
+      'effectHeight': effectHeight,
+    };
+    return 'CharSize$data';
+  }
+}

+ 23 - 0
lib/frontend/helpers.dart

@@ -0,0 +1,23 @@
+import 'package:flutter/widgets.dart';
+
+Size textSize(Text text) {
+  var span = text.textSpan ?? TextSpan(text: text.data, style: text.style);
+
+  var tp = TextPainter(
+    text: span,
+    textAlign: text.textAlign ?? TextAlign.start,
+    textDirection: text.textDirection ?? TextDirection.ltr,
+    textScaleFactor: text.textScaleFactor ?? 1,
+    maxLines: text.maxLines,
+    locale: text.locale,
+    strutStyle: text.strutStyle,
+  );
+
+  tp.layout();
+
+  return Size(tp.width, tp.height);
+}
+
+bool isMonospace(List<String> fontFamily) {
+  return true; // TBD
+}

+ 131 - 0
lib/frontend/input_listener.dart

@@ -0,0 +1,131 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart';
+
+typedef KeyStrokeHandler = void Function(RawKeyEvent);
+typedef InputHandler = void Function(String);
+typedef FocusHandler = void Function(bool);
+
+class InputListener extends StatefulWidget {
+  InputListener({
+    @required this.child,
+    @required this.onKeyStroke,
+    @required this.onInput,
+    @required this.focusNode,
+    this.onFocus,
+    this.autofocus = false,
+  });
+
+  final Widget child;
+  final InputHandler onInput;
+  final KeyStrokeHandler onKeyStroke;
+  final FocusHandler onFocus;
+  final bool autofocus;
+  final FocusNode focusNode;
+
+  @override
+  InputListenerState createState() => InputListenerState();
+}
+
+class InputListenerState extends State<InputListener> {
+  var focused = false;
+  TextInputConnection conn;
+
+  @override
+  void initState() {
+    widget.focusNode.addListener(onFocus);
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return RawKeyboardListener(
+      focusNode: widget.focusNode,
+      onKey: widget.onKeyStroke,
+      autofocus: widget.autofocus,
+      child: widget.child,
+    );
+  }
+
+  void onFocus() {
+    if (focused == widget.focusNode.hasFocus) {
+      return;
+    }
+
+    focused = widget.focusNode.hasFocus;
+
+    if (widget.onFocus != null) {
+      widget.onFocus(focused);
+    }
+
+    openTextInput();
+  }
+
+  void openTextInput() {
+    final config = TextInputConfiguration();
+    conn = TextInput.attach(
+      TerminalTextInputClient(onInput),
+      config,
+    );
+
+    final dx = 0.0;
+    final dy = 0.0;
+    conn.setEditableSizeAndTransform(
+      Size(10, 10),
+      Matrix4.translationValues(dx, dy, 0.0),
+    );
+
+    conn.show();
+  }
+
+  void onInput(String text) {
+    widget.onInput(text);
+    conn?.setEditingState(TextEditingValue.empty);
+  }
+}
+
+class TerminalTextInputClient extends TextInputClient {
+  TerminalTextInputClient(this.onInput);
+
+  final InputHandler onInput;
+
+  TextEditingValue _savedValue;
+
+  TextEditingValue get currentTextEditingValue {
+    return _savedValue;
+  }
+
+  AutofillScope get currentAutofillScope {
+    return null;
+  }
+
+  void updateEditingValue(TextEditingValue value) {
+    // print('updateEditingValue $value');
+
+    if (_savedValue == null) {
+      onInput(value.text);
+    } else if (_savedValue.text.length < value.text.length) {
+      final diff = value.text.substring(_savedValue.text.length);
+      onInput(diff);
+    }
+
+    _savedValue = value;
+    // print('updateEditingValue $value');
+  }
+
+  void performAction(TextInputAction action) {
+    // print('performAction $action');
+  }
+
+  void updateFloatingCursor(RawFloatingCursorPoint point) {
+    // print('updateFloatingCursor');
+  }
+
+  void showAutocorrectionPromptRect(int start, int end) {
+    // print('showAutocorrectionPromptRect');
+  }
+
+  void connectionClosed() {
+    // print('connectionClosed');
+  }
+}

+ 287 - 0
lib/frontend/input_map.dart

@@ -0,0 +1,287 @@
+import 'package:flutter/services.dart';
+import 'package:xterm/input/keys.dart';
+
+final _idKeyMap = {
+  LogicalKeyboardKey.none.keyId: TerminalKey.none,
+  LogicalKeyboardKey.hyper.keyId: TerminalKey.hyper,
+  LogicalKeyboardKey.superKey.keyId: TerminalKey.superKey,
+  LogicalKeyboardKey.fnLock.keyId: TerminalKey.fnLock,
+  LogicalKeyboardKey.suspend.keyId: TerminalKey.suspend,
+  LogicalKeyboardKey.resume.keyId: TerminalKey.resume,
+  LogicalKeyboardKey.turbo.keyId: TerminalKey.turbo,
+  LogicalKeyboardKey.privacyScreenToggle.keyId: TerminalKey.privacyScreenToggle,
+  LogicalKeyboardKey.sleep.keyId: TerminalKey.sleep,
+  LogicalKeyboardKey.wakeUp.keyId: TerminalKey.wakeUp,
+  LogicalKeyboardKey.displayToggleIntExt.keyId: TerminalKey.displayToggleIntExt,
+  LogicalKeyboardKey.usbReserved.keyId: TerminalKey.usbReserved,
+  LogicalKeyboardKey.usbErrorRollOver.keyId: TerminalKey.usbErrorRollOver,
+  LogicalKeyboardKey.usbPostFail.keyId: TerminalKey.usbPostFail,
+  LogicalKeyboardKey.usbErrorUndefined.keyId: TerminalKey.usbErrorUndefined,
+  LogicalKeyboardKey.keyA.keyId: TerminalKey.keyA,
+  LogicalKeyboardKey.keyB.keyId: TerminalKey.keyB,
+  LogicalKeyboardKey.keyC.keyId: TerminalKey.keyC,
+  LogicalKeyboardKey.keyD.keyId: TerminalKey.keyD,
+  LogicalKeyboardKey.keyE.keyId: TerminalKey.keyE,
+  LogicalKeyboardKey.keyF.keyId: TerminalKey.keyF,
+  LogicalKeyboardKey.keyG.keyId: TerminalKey.keyG,
+  LogicalKeyboardKey.keyH.keyId: TerminalKey.keyH,
+  LogicalKeyboardKey.keyI.keyId: TerminalKey.keyI,
+  LogicalKeyboardKey.keyJ.keyId: TerminalKey.keyJ,
+  LogicalKeyboardKey.keyK.keyId: TerminalKey.keyK,
+  LogicalKeyboardKey.keyL.keyId: TerminalKey.keyL,
+  LogicalKeyboardKey.keyM.keyId: TerminalKey.keyM,
+  LogicalKeyboardKey.keyN.keyId: TerminalKey.keyN,
+  LogicalKeyboardKey.keyO.keyId: TerminalKey.keyO,
+  LogicalKeyboardKey.keyP.keyId: TerminalKey.keyP,
+  LogicalKeyboardKey.keyQ.keyId: TerminalKey.keyQ,
+  LogicalKeyboardKey.keyR.keyId: TerminalKey.keyR,
+  LogicalKeyboardKey.keyS.keyId: TerminalKey.keyS,
+  LogicalKeyboardKey.keyT.keyId: TerminalKey.keyT,
+  LogicalKeyboardKey.keyU.keyId: TerminalKey.keyU,
+  LogicalKeyboardKey.keyV.keyId: TerminalKey.keyV,
+  LogicalKeyboardKey.keyW.keyId: TerminalKey.keyW,
+  LogicalKeyboardKey.keyX.keyId: TerminalKey.keyX,
+  LogicalKeyboardKey.keyY.keyId: TerminalKey.keyY,
+  LogicalKeyboardKey.keyZ.keyId: TerminalKey.keyZ,
+  LogicalKeyboardKey.digit1.keyId: TerminalKey.digit1,
+  LogicalKeyboardKey.digit2.keyId: TerminalKey.digit2,
+  LogicalKeyboardKey.digit3.keyId: TerminalKey.digit3,
+  LogicalKeyboardKey.digit4.keyId: TerminalKey.digit4,
+  LogicalKeyboardKey.digit5.keyId: TerminalKey.digit5,
+  LogicalKeyboardKey.digit6.keyId: TerminalKey.digit6,
+  LogicalKeyboardKey.digit7.keyId: TerminalKey.digit7,
+  LogicalKeyboardKey.digit8.keyId: TerminalKey.digit8,
+  LogicalKeyboardKey.digit9.keyId: TerminalKey.digit9,
+  LogicalKeyboardKey.digit0.keyId: TerminalKey.digit0,
+  LogicalKeyboardKey.enter.keyId: TerminalKey.enter,
+  LogicalKeyboardKey.escape.keyId: TerminalKey.escape,
+  LogicalKeyboardKey.backspace.keyId: TerminalKey.backspace,
+  LogicalKeyboardKey.tab.keyId: TerminalKey.tab,
+  LogicalKeyboardKey.space.keyId: TerminalKey.space,
+  LogicalKeyboardKey.minus.keyId: TerminalKey.minus,
+  LogicalKeyboardKey.equal.keyId: TerminalKey.equal,
+  LogicalKeyboardKey.bracketLeft.keyId: TerminalKey.bracketLeft,
+  LogicalKeyboardKey.bracketRight.keyId: TerminalKey.bracketRight,
+  LogicalKeyboardKey.backslash.keyId: TerminalKey.backslash,
+  LogicalKeyboardKey.semicolon.keyId: TerminalKey.semicolon,
+  LogicalKeyboardKey.quote.keyId: TerminalKey.quote,
+  LogicalKeyboardKey.backquote.keyId: TerminalKey.backquote,
+  LogicalKeyboardKey.comma.keyId: TerminalKey.comma,
+  LogicalKeyboardKey.period.keyId: TerminalKey.period,
+  LogicalKeyboardKey.slash.keyId: TerminalKey.slash,
+  LogicalKeyboardKey.capsLock.keyId: TerminalKey.capsLock,
+  LogicalKeyboardKey.f1.keyId: TerminalKey.f1,
+  LogicalKeyboardKey.f2.keyId: TerminalKey.f2,
+  LogicalKeyboardKey.f3.keyId: TerminalKey.f3,
+  LogicalKeyboardKey.f4.keyId: TerminalKey.f4,
+  LogicalKeyboardKey.f5.keyId: TerminalKey.f5,
+  LogicalKeyboardKey.f6.keyId: TerminalKey.f6,
+  LogicalKeyboardKey.f7.keyId: TerminalKey.f7,
+  LogicalKeyboardKey.f8.keyId: TerminalKey.f8,
+  LogicalKeyboardKey.f9.keyId: TerminalKey.f9,
+  LogicalKeyboardKey.f10.keyId: TerminalKey.f10,
+  LogicalKeyboardKey.f11.keyId: TerminalKey.f11,
+  LogicalKeyboardKey.f12.keyId: TerminalKey.f12,
+  LogicalKeyboardKey.printScreen.keyId: TerminalKey.printScreen,
+  LogicalKeyboardKey.scrollLock.keyId: TerminalKey.scrollLock,
+  LogicalKeyboardKey.pause.keyId: TerminalKey.pause,
+  LogicalKeyboardKey.insert.keyId: TerminalKey.insert,
+  LogicalKeyboardKey.home.keyId: TerminalKey.home,
+  LogicalKeyboardKey.pageUp.keyId: TerminalKey.pageUp,
+  LogicalKeyboardKey.delete.keyId: TerminalKey.delete,
+  LogicalKeyboardKey.end.keyId: TerminalKey.end,
+  LogicalKeyboardKey.pageDown.keyId: TerminalKey.pageDown,
+  LogicalKeyboardKey.arrowRight.keyId: TerminalKey.arrowRight,
+  LogicalKeyboardKey.arrowLeft.keyId: TerminalKey.arrowLeft,
+  LogicalKeyboardKey.arrowDown.keyId: TerminalKey.arrowDown,
+  LogicalKeyboardKey.arrowUp.keyId: TerminalKey.arrowUp,
+  LogicalKeyboardKey.numLock.keyId: TerminalKey.numLock,
+  LogicalKeyboardKey.numpadDivide.keyId: TerminalKey.numpadDivide,
+  LogicalKeyboardKey.numpadMultiply.keyId: TerminalKey.numpadMultiply,
+  LogicalKeyboardKey.numpadSubtract.keyId: TerminalKey.numpadSubtract,
+  LogicalKeyboardKey.numpadAdd.keyId: TerminalKey.numpadAdd,
+  LogicalKeyboardKey.numpadEnter.keyId: TerminalKey.numpadEnter,
+  LogicalKeyboardKey.numpad1.keyId: TerminalKey.numpad1,
+  LogicalKeyboardKey.numpad2.keyId: TerminalKey.numpad2,
+  LogicalKeyboardKey.numpad3.keyId: TerminalKey.numpad3,
+  LogicalKeyboardKey.numpad4.keyId: TerminalKey.numpad4,
+  LogicalKeyboardKey.numpad5.keyId: TerminalKey.numpad5,
+  LogicalKeyboardKey.numpad6.keyId: TerminalKey.numpad6,
+  LogicalKeyboardKey.numpad7.keyId: TerminalKey.numpad7,
+  LogicalKeyboardKey.numpad8.keyId: TerminalKey.numpad8,
+  LogicalKeyboardKey.numpad9.keyId: TerminalKey.numpad9,
+  LogicalKeyboardKey.numpad0.keyId: TerminalKey.numpad0,
+  LogicalKeyboardKey.numpadDecimal.keyId: TerminalKey.numpadDecimal,
+  LogicalKeyboardKey.intlBackslash.keyId: TerminalKey.intlBackslash,
+  LogicalKeyboardKey.contextMenu.keyId: TerminalKey.contextMenu,
+  LogicalKeyboardKey.power.keyId: TerminalKey.power,
+  LogicalKeyboardKey.numpadEqual.keyId: TerminalKey.numpadEqual,
+  LogicalKeyboardKey.f13.keyId: TerminalKey.f13,
+  LogicalKeyboardKey.f14.keyId: TerminalKey.f14,
+  LogicalKeyboardKey.f15.keyId: TerminalKey.f15,
+  LogicalKeyboardKey.f16.keyId: TerminalKey.f16,
+  LogicalKeyboardKey.f17.keyId: TerminalKey.f17,
+  LogicalKeyboardKey.f18.keyId: TerminalKey.f18,
+  LogicalKeyboardKey.f19.keyId: TerminalKey.f19,
+  LogicalKeyboardKey.f20.keyId: TerminalKey.f20,
+  LogicalKeyboardKey.f21.keyId: TerminalKey.f21,
+  LogicalKeyboardKey.f22.keyId: TerminalKey.f22,
+  LogicalKeyboardKey.f23.keyId: TerminalKey.f23,
+  LogicalKeyboardKey.f24.keyId: TerminalKey.f24,
+  LogicalKeyboardKey.open.keyId: TerminalKey.open,
+  LogicalKeyboardKey.help.keyId: TerminalKey.help,
+  LogicalKeyboardKey.select.keyId: TerminalKey.select,
+  LogicalKeyboardKey.again.keyId: TerminalKey.again,
+  LogicalKeyboardKey.undo.keyId: TerminalKey.undo,
+  LogicalKeyboardKey.cut.keyId: TerminalKey.cut,
+  LogicalKeyboardKey.copy.keyId: TerminalKey.copy,
+  LogicalKeyboardKey.paste.keyId: TerminalKey.paste,
+  LogicalKeyboardKey.find.keyId: TerminalKey.find,
+  LogicalKeyboardKey.audioVolumeMute.keyId: TerminalKey.audioVolumeMute,
+  LogicalKeyboardKey.audioVolumeUp.keyId: TerminalKey.audioVolumeUp,
+  LogicalKeyboardKey.audioVolumeDown.keyId: TerminalKey.audioVolumeDown,
+  LogicalKeyboardKey.numpadComma.keyId: TerminalKey.numpadComma,
+  LogicalKeyboardKey.intlRo.keyId: TerminalKey.intlRo,
+  LogicalKeyboardKey.kanaMode.keyId: TerminalKey.kanaMode,
+  LogicalKeyboardKey.intlYen.keyId: TerminalKey.intlYen,
+  LogicalKeyboardKey.convert.keyId: TerminalKey.convert,
+  LogicalKeyboardKey.nonConvert.keyId: TerminalKey.nonConvert,
+  LogicalKeyboardKey.lang1.keyId: TerminalKey.lang1,
+  LogicalKeyboardKey.lang2.keyId: TerminalKey.lang2,
+  LogicalKeyboardKey.lang3.keyId: TerminalKey.lang3,
+  LogicalKeyboardKey.lang4.keyId: TerminalKey.lang4,
+  LogicalKeyboardKey.lang5.keyId: TerminalKey.lang5,
+  LogicalKeyboardKey.abort.keyId: TerminalKey.abort,
+  LogicalKeyboardKey.props.keyId: TerminalKey.props,
+  LogicalKeyboardKey.numpadParenLeft.keyId: TerminalKey.numpadParenLeft,
+  LogicalKeyboardKey.numpadParenRight.keyId: TerminalKey.numpadParenRight,
+  LogicalKeyboardKey.numpadBackspace.keyId: TerminalKey.numpadBackspace,
+  LogicalKeyboardKey.numpadMemoryStore.keyId: TerminalKey.numpadMemoryStore,
+  LogicalKeyboardKey.numpadMemoryRecall.keyId: TerminalKey.numpadMemoryRecall,
+  LogicalKeyboardKey.numpadMemoryClear.keyId: TerminalKey.numpadMemoryClear,
+  LogicalKeyboardKey.numpadMemoryAdd.keyId: TerminalKey.numpadMemoryAdd,
+  LogicalKeyboardKey.numpadMemorySubtract.keyId:
+      TerminalKey.numpadMemorySubtract,
+  LogicalKeyboardKey.numpadSignChange.keyId: TerminalKey.numpadSignChange,
+  LogicalKeyboardKey.numpadClear.keyId: TerminalKey.numpadClear,
+  LogicalKeyboardKey.numpadClearEntry.keyId: TerminalKey.numpadClearEntry,
+  LogicalKeyboardKey.controlLeft.keyId: TerminalKey.controlLeft,
+  LogicalKeyboardKey.shiftLeft.keyId: TerminalKey.shiftLeft,
+  LogicalKeyboardKey.altLeft.keyId: TerminalKey.altLeft,
+  LogicalKeyboardKey.metaLeft.keyId: TerminalKey.metaLeft,
+  LogicalKeyboardKey.controlRight.keyId: TerminalKey.controlRight,
+  LogicalKeyboardKey.shiftRight.keyId: TerminalKey.shiftRight,
+  LogicalKeyboardKey.altRight.keyId: TerminalKey.altRight,
+  LogicalKeyboardKey.metaRight.keyId: TerminalKey.metaRight,
+  LogicalKeyboardKey.info.keyId: TerminalKey.info,
+  LogicalKeyboardKey.closedCaptionToggle.keyId: TerminalKey.closedCaptionToggle,
+  LogicalKeyboardKey.brightnessUp.keyId: TerminalKey.brightnessUp,
+  LogicalKeyboardKey.brightnessDown.keyId: TerminalKey.brightnessDown,
+  LogicalKeyboardKey.brightnessToggle.keyId: TerminalKey.brightnessToggle,
+  LogicalKeyboardKey.brightnessMinimum.keyId: TerminalKey.brightnessMinimum,
+  LogicalKeyboardKey.brightnessMaximum.keyId: TerminalKey.brightnessMaximum,
+  LogicalKeyboardKey.brightnessAuto.keyId: TerminalKey.brightnessAuto,
+  LogicalKeyboardKey.mediaLast.keyId: TerminalKey.mediaLast,
+  LogicalKeyboardKey.launchPhone.keyId: TerminalKey.launchPhone,
+  LogicalKeyboardKey.programGuide.keyId: TerminalKey.programGuide,
+  LogicalKeyboardKey.exit.keyId: TerminalKey.exit,
+  LogicalKeyboardKey.channelUp.keyId: TerminalKey.channelUp,
+  LogicalKeyboardKey.channelDown.keyId: TerminalKey.channelDown,
+  LogicalKeyboardKey.mediaPlay.keyId: TerminalKey.mediaPlay,
+  LogicalKeyboardKey.mediaPause.keyId: TerminalKey.mediaPause,
+  LogicalKeyboardKey.mediaRecord.keyId: TerminalKey.mediaRecord,
+  LogicalKeyboardKey.mediaFastForward.keyId: TerminalKey.mediaFastForward,
+  LogicalKeyboardKey.mediaRewind.keyId: TerminalKey.mediaRewind,
+  LogicalKeyboardKey.mediaTrackNext.keyId: TerminalKey.mediaTrackNext,
+  LogicalKeyboardKey.mediaTrackPrevious.keyId: TerminalKey.mediaTrackPrevious,
+  LogicalKeyboardKey.mediaStop.keyId: TerminalKey.mediaStop,
+  LogicalKeyboardKey.eject.keyId: TerminalKey.eject,
+  LogicalKeyboardKey.mediaPlayPause.keyId: TerminalKey.mediaPlayPause,
+  LogicalKeyboardKey.speechInputToggle.keyId: TerminalKey.speechInputToggle,
+  LogicalKeyboardKey.bassBoost.keyId: TerminalKey.bassBoost,
+  LogicalKeyboardKey.mediaSelect.keyId: TerminalKey.mediaSelect,
+  LogicalKeyboardKey.launchWordProcessor.keyId: TerminalKey.launchWordProcessor,
+  LogicalKeyboardKey.launchSpreadsheet.keyId: TerminalKey.launchSpreadsheet,
+  LogicalKeyboardKey.launchMail.keyId: TerminalKey.launchMail,
+  LogicalKeyboardKey.launchContacts.keyId: TerminalKey.launchContacts,
+  LogicalKeyboardKey.launchCalendar.keyId: TerminalKey.launchCalendar,
+  LogicalKeyboardKey.launchApp2.keyId: TerminalKey.launchApp2,
+  LogicalKeyboardKey.launchApp1.keyId: TerminalKey.launchApp1,
+  LogicalKeyboardKey.launchInternetBrowser.keyId:
+      TerminalKey.launchInternetBrowser,
+  LogicalKeyboardKey.logOff.keyId: TerminalKey.logOff,
+  LogicalKeyboardKey.lockScreen.keyId: TerminalKey.lockScreen,
+  LogicalKeyboardKey.launchControlPanel.keyId: TerminalKey.launchControlPanel,
+  LogicalKeyboardKey.selectTask.keyId: TerminalKey.selectTask,
+  LogicalKeyboardKey.launchDocuments.keyId: TerminalKey.launchDocuments,
+  LogicalKeyboardKey.spellCheck.keyId: TerminalKey.spellCheck,
+  LogicalKeyboardKey.launchKeyboardLayout.keyId:
+      TerminalKey.launchKeyboardLayout,
+  LogicalKeyboardKey.launchScreenSaver.keyId: TerminalKey.launchScreenSaver,
+  LogicalKeyboardKey.launchAssistant.keyId: TerminalKey.launchAssistant,
+  LogicalKeyboardKey.launchAudioBrowser.keyId: TerminalKey.launchAudioBrowser,
+  LogicalKeyboardKey.newKey.keyId: TerminalKey.newKey,
+  LogicalKeyboardKey.close.keyId: TerminalKey.close,
+  LogicalKeyboardKey.save.keyId: TerminalKey.save,
+  LogicalKeyboardKey.print.keyId: TerminalKey.print,
+  LogicalKeyboardKey.browserSearch.keyId: TerminalKey.browserSearch,
+  LogicalKeyboardKey.browserHome.keyId: TerminalKey.browserHome,
+  LogicalKeyboardKey.browserBack.keyId: TerminalKey.browserBack,
+  LogicalKeyboardKey.browserForward.keyId: TerminalKey.browserForward,
+  LogicalKeyboardKey.browserStop.keyId: TerminalKey.browserStop,
+  LogicalKeyboardKey.browserRefresh.keyId: TerminalKey.browserRefresh,
+  LogicalKeyboardKey.browserFavorites.keyId: TerminalKey.browserFavorites,
+  LogicalKeyboardKey.zoomIn.keyId: TerminalKey.zoomIn,
+  LogicalKeyboardKey.zoomOut.keyId: TerminalKey.zoomOut,
+  LogicalKeyboardKey.zoomToggle.keyId: TerminalKey.zoomToggle,
+  LogicalKeyboardKey.redo.keyId: TerminalKey.redo,
+  LogicalKeyboardKey.mailReply.keyId: TerminalKey.mailReply,
+  LogicalKeyboardKey.mailForward.keyId: TerminalKey.mailForward,
+  LogicalKeyboardKey.mailSend.keyId: TerminalKey.mailSend,
+  LogicalKeyboardKey.keyboardLayoutSelect.keyId:
+      TerminalKey.keyboardLayoutSelect,
+  LogicalKeyboardKey.showAllWindows.keyId: TerminalKey.showAllWindows,
+  LogicalKeyboardKey.gameButton1.keyId: TerminalKey.gameButton1,
+  LogicalKeyboardKey.gameButton2.keyId: TerminalKey.gameButton2,
+  LogicalKeyboardKey.gameButton3.keyId: TerminalKey.gameButton3,
+  LogicalKeyboardKey.gameButton4.keyId: TerminalKey.gameButton4,
+  LogicalKeyboardKey.gameButton5.keyId: TerminalKey.gameButton5,
+  LogicalKeyboardKey.gameButton6.keyId: TerminalKey.gameButton6,
+  LogicalKeyboardKey.gameButton7.keyId: TerminalKey.gameButton7,
+  LogicalKeyboardKey.gameButton8.keyId: TerminalKey.gameButton8,
+  LogicalKeyboardKey.gameButton9.keyId: TerminalKey.gameButton9,
+  LogicalKeyboardKey.gameButton10.keyId: TerminalKey.gameButton10,
+  LogicalKeyboardKey.gameButton11.keyId: TerminalKey.gameButton11,
+  LogicalKeyboardKey.gameButton12.keyId: TerminalKey.gameButton12,
+  LogicalKeyboardKey.gameButton13.keyId: TerminalKey.gameButton13,
+  LogicalKeyboardKey.gameButton14.keyId: TerminalKey.gameButton14,
+  LogicalKeyboardKey.gameButton15.keyId: TerminalKey.gameButton15,
+  LogicalKeyboardKey.gameButton16.keyId: TerminalKey.gameButton16,
+  LogicalKeyboardKey.gameButtonA.keyId: TerminalKey.gameButtonA,
+  LogicalKeyboardKey.gameButtonB.keyId: TerminalKey.gameButtonB,
+  LogicalKeyboardKey.gameButtonC.keyId: TerminalKey.gameButtonC,
+  LogicalKeyboardKey.gameButtonLeft1.keyId: TerminalKey.gameButtonLeft1,
+  LogicalKeyboardKey.gameButtonLeft2.keyId: TerminalKey.gameButtonLeft2,
+  LogicalKeyboardKey.gameButtonMode.keyId: TerminalKey.gameButtonMode,
+  LogicalKeyboardKey.gameButtonRight1.keyId: TerminalKey.gameButtonRight1,
+  LogicalKeyboardKey.gameButtonRight2.keyId: TerminalKey.gameButtonRight2,
+  LogicalKeyboardKey.gameButtonSelect.keyId: TerminalKey.gameButtonSelect,
+  LogicalKeyboardKey.gameButtonStart.keyId: TerminalKey.gameButtonStart,
+  LogicalKeyboardKey.gameButtonThumbLeft.keyId: TerminalKey.gameButtonThumbLeft,
+  LogicalKeyboardKey.gameButtonThumbRight.keyId:
+      TerminalKey.gameButtonThumbRight,
+  LogicalKeyboardKey.gameButtonX.keyId: TerminalKey.gameButtonX,
+  LogicalKeyboardKey.gameButtonY.keyId: TerminalKey.gameButtonY,
+  LogicalKeyboardKey.gameButtonZ.keyId: TerminalKey.gameButtonZ,
+  LogicalKeyboardKey.fn.keyId: TerminalKey.fn,
+  LogicalKeyboardKey.shift.keyId: TerminalKey.shift,
+  LogicalKeyboardKey.meta.keyId: TerminalKey.meta,
+  LogicalKeyboardKey.alt.keyId: TerminalKey.alt,
+  LogicalKeyboardKey.control.keyId: TerminalKey.control,
+  // LogicalKeyboardKey.backtab.keyId: TerminalKey.backtab,
+  // LogicalKeyboardKey.returnKey.keyId: TerminalKey.returnKey,
+};
+
+TerminalKey inputMap(LogicalKeyboardKey key) {
+  return _idKeyMap[key.keyId];
+}

+ 25 - 0
lib/frontend/mouse_listener.dart

@@ -0,0 +1,25 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/widgets.dart';
+
+typedef ScrollHandler = void Function(Offset);
+
+class MouseListener extends StatelessWidget {
+  MouseListener({this.child, this.onScroll});
+
+  final Widget child;
+  final ScrollHandler onScroll;
+
+  @override
+  Widget build(BuildContext context) {
+    return Listener(
+      child: child,
+      onPointerSignal: onPointerSignal,
+    );
+  }
+
+  void onPointerSignal(PointerSignalEvent event) {
+    if (event is PointerScrollEvent && onScroll != null) {
+      onScroll(event.scrollDelta);
+    }
+  }
+}

+ 33 - 0
lib/frontend/oscillator.dart

@@ -0,0 +1,33 @@
+import 'dart:async';
+
+import 'package:xterm/utli/observable.dart';
+
+class Oscillator with Observable {
+  Oscillator(this.duration);
+
+  Oscillator.ms(int ms) : duration = Duration(milliseconds: ms);
+
+  final Duration duration;
+
+  var _value = true;
+  Timer _timer;
+
+  void _onOscillation(_) {
+    _value = !_value;
+    notifyListeners();
+  }
+
+  bool get value {
+    return _value;
+  }
+
+  void start() {
+    if (_timer != null) return;
+    _timer = Timer.periodic(duration, _onOscillation);
+  }
+
+  void stop() {
+    _timer.cancel();
+    _timer = null;
+  }
+}

+ 509 - 0
lib/frontend/terminal_view.dart

@@ -0,0 +1,509 @@
+import 'dart:math' as math;
+import 'dart:ui';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/services.dart';
+import 'package:meta/meta.dart';
+import 'package:xterm/buffer/cell.dart';
+import 'package:xterm/frontend/char_size.dart';
+import 'package:xterm/frontend/helpers.dart';
+import 'package:xterm/frontend/input_listener.dart';
+import 'package:xterm/frontend/input_map.dart';
+import 'package:xterm/frontend/mouse_listener.dart';
+import 'package:xterm/frontend/oscillator.dart';
+import 'package:xterm/frontend/text_layout_cache.dart';
+import 'package:xterm/mouse/position.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+typedef ResizeHandler = void Function(int width, int height);
+
+const _kDefaultFontFamily = [
+  'Droid Sans Mono',
+  'Noto Sans Mono',
+  'Roboto Mono',
+  'Consolas',
+  'Noto Sans Mono CJK SC',
+  'Noto Sans Mono CJK TC',
+  'Noto Sans Mono CJK KR',
+  'Noto Sans Mono CJK JP',
+  'Noto Sans Mono CJK HK',
+  'monospace',
+  'Noto Color Emoji',
+  'Noto Sans Symbols',
+  'Roboto',
+  'Ubuntu',
+  'Cantarell',
+  'DejaVu Sans',
+  'Liberation Sans',
+  'Arial',
+  'Droid Sans Fallback',
+  'sans-serif',
+];
+
+class TerminalView extends StatefulWidget {
+  TerminalView({
+    Key key,
+    @required this.terminal,
+    this.onResize,
+    this.fontSize = 16,
+    this.fontFamily = _kDefaultFontFamily,
+    this.fontWidthScaleFactor = 1.0,
+    this.fontHeightScaleFactor = 1.1,
+  }) : super(key: key ?? ValueKey(terminal));
+
+  final Terminal terminal;
+  final ResizeHandler onResize;
+
+  final double fontSize;
+  final double fontWidthScaleFactor;
+  final double fontHeightScaleFactor;
+  final List<String> fontFamily;
+
+  CharSize getCharSize() {
+    final testString = 'xxxxxxxxxx' * 1000;
+    final text = Text(
+      testString,
+      style: TextStyle(
+        fontFamilyFallback: _kDefaultFontFamily,
+        fontSize: fontSize,
+      ),
+    );
+    final size = textSize(text);
+
+    final width = (size.width / testString.length);
+    final height = size.height;
+
+    final effectWidth = width * fontWidthScaleFactor;
+    final effectHeight = size.height * fontHeightScaleFactor;
+
+    // final ls
+
+    return CharSize(
+      width: width,
+      height: height,
+      effectWidth: effectWidth,
+      effectHeight: effectHeight,
+      letterSpacing: effectWidth - width,
+      lineSpacing: effectHeight - height,
+    );
+  }
+
+  @override
+  _TerminalViewState createState() => _TerminalViewState();
+}
+
+class _TerminalViewState extends State<TerminalView> {
+  final oscillator = Oscillator.ms(600);
+  final focusNode = FocusNode();
+  var focused = false;
+
+  int _lastTerminalWidth;
+  int _lastTerminalHeight;
+  CharSize _charSize;
+
+  void onTerminalChange() {
+    if (mounted) {
+      setState(() {});
+    }
+  }
+
+  void onTick() {
+    widget.terminal.refresh();
+  }
+
+  @override
+  void initState() {
+    // oscillator.start();
+    // oscillator.addListener(onTick);
+    _charSize = widget.getCharSize();
+    widget.terminal.addListener(onTerminalChange);
+    super.initState();
+  }
+
+  @override
+  void didUpdateWidget(TerminalView oldWidget) {
+    widget.terminal.addListener(onTerminalChange);
+    super.didUpdateWidget(oldWidget);
+  }
+
+  @override
+  void dispose() {
+    // oscillator.stop();
+    // oscillator.removeListener(onTick);
+
+    widget.terminal.removeListener(onTerminalChange);
+
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    Widget result = Container(
+      constraints: BoxConstraints.expand(),
+      color: Color(widget.terminal.colorScheme.background.value),
+      child: CustomPaint(
+        painter: TerminalPainter(
+          terminal: widget.terminal,
+          view: widget,
+          oscillator: oscillator,
+          focused: focused,
+          charSize: _charSize,
+        ),
+      ),
+    );
+
+    result = GestureDetector(
+      child: result,
+      behavior: HitTestBehavior.deferToChild,
+      dragStartBehavior: DragStartBehavior.down,
+      onTapDown: (detail) {
+        focusNode.requestFocus();
+        final pos = detail.localPosition;
+        final offset = getMouseOffset(pos.dx, pos.dy);
+        widget.terminal.mouseMode.onTap(widget.terminal, offset);
+        widget.terminal.refresh();
+      },
+      onPanStart: (detail) {
+        final pos = detail.localPosition;
+        final offset = getMouseOffset(pos.dx, pos.dy);
+        widget.terminal.mouseMode.onPanStart(widget.terminal, offset);
+        widget.terminal.refresh();
+      },
+      onPanUpdate: (detail) {
+        final pos = detail.localPosition;
+        final offset = getMouseOffset(pos.dx, pos.dy);
+        widget.terminal.mouseMode.onPanUpdate(widget.terminal, offset);
+        widget.terminal.refresh();
+      },
+    );
+
+    return InputListener(
+      onKeyStroke: onKeyStroke,
+      onInput: onInput,
+      onFocus: onFocus,
+      focusNode: focusNode,
+      autofocus: true,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.text,
+        child: MouseListener(
+          onScroll: onScroll,
+          child: LayoutBuilder(builder: (context, constraints) {
+            onResize(constraints.maxWidth, constraints.maxHeight);
+            return result;
+          }),
+        ),
+      ),
+    );
+  }
+
+  Position getMouseOffset(double px, double py) {
+    final col = (px / _charSize.effectWidth).floor();
+    final row = (py / _charSize.effectHeight).floor();
+
+    final x = col;
+    final y = widget.terminal.buffer.convertViewLineToRawLine(row) -
+        widget.terminal.buffer.scrollOffset;
+
+    return Position(x, y);
+  }
+
+  void onResize(double width, double height) {
+    final termWidth = (width / _charSize.effectWidth).floor();
+    final termHeight = (height / _charSize.effectHeight).floor();
+
+    if (_lastTerminalWidth != termWidth || _lastTerminalHeight != termHeight) {
+      _lastTerminalWidth = termWidth;
+      _lastTerminalHeight = termHeight;
+
+      // print('($termWidth, $termHeight)');
+
+      if (widget.onResize != null) {
+        widget.onResize(termWidth, termHeight);
+      }
+
+      // SchedulerBinding.instance.addPostFrameCallback((_) {
+      //   widget.terminal.resize(termWidth, termHeight);
+      // });
+
+      Future.delayed(Duration.zero).then((_) {
+        widget.terminal.resize(termWidth, termHeight);
+      });
+    }
+  }
+
+  void onInput(String input) {
+    widget.terminal.onInput(input);
+  }
+
+  void onKeyStroke(RawKeyEvent event) {
+    if (event is! RawKeyDownEvent) {
+      return;
+    }
+
+    final key = inputMap(event.logicalKey);
+    widget.terminal.debug.onMsg(key);
+    if (key != null) {
+      widget.terminal.input(
+        key,
+        ctrl: event.isControlPressed,
+        alt: event.isAltPressed,
+        shift: event.isShiftPressed,
+      );
+    }
+
+    widget.terminal.buffer.setScrollOffset(0);
+  }
+
+  void onFocus(bool focused) {
+    this.focused = focused;
+    widget.terminal.debug.onMsg('focused $focused');
+    widget.terminal.refresh();
+  }
+
+  void onScroll(Offset offset) {
+    final delta = math.max(1, offset.dy.abs() ~/ 10);
+
+    if (offset.dy > 0) {
+      widget.terminal.buffer.screenScrollDown(delta);
+    } else if (offset.dy < 0) {
+      widget.terminal.buffer.screenScrollUp(delta);
+    }
+  }
+}
+
+final textLayoutCache = TextLayoutCache(TextDirection.ltr, 1024);
+
+class TerminalPainter extends CustomPainter {
+  TerminalPainter({
+    this.terminal,
+    this.view,
+    this.oscillator,
+    this.focused,
+    this.charSize,
+  });
+
+  final Terminal terminal;
+  final TerminalView view;
+  final Oscillator oscillator;
+  final bool focused;
+  final CharSize charSize;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    paintBackground(canvas);
+
+    // if (oscillator.value) {
+    // }
+
+    if (terminal.showCursor) {
+      paintCursor(canvas);
+    }
+
+    paintText(canvas);
+    // or paintTextFast(canvas);
+
+    paintSelection(canvas);
+  }
+
+  void paintBackground(Canvas canvas) {
+    final lines = terminal.getVisibleLines();
+
+    for (var i = 0; i < lines.length; i++) {
+      final line = lines[i];
+      final offsetY = i * charSize.effectHeight;
+      final cellCount = math.min(terminal.viewWidth, line.length);
+
+      for (var i = 0; i < cellCount; i++) {
+        final cell = line.getCell(i);
+
+        if (cell.attr == null || cell.width == 0) {
+          continue;
+        }
+
+        final offsetX = i * charSize.effectWidth;
+        final effectWidth = charSize.effectWidth * cell.width + 1;
+        final effectHeight = charSize.effectHeight + 1;
+
+        final bgColor =
+            cell.attr.inverse ? cell.attr.fgColor : cell.attr.bgColor;
+
+        if (bgColor == null) {
+          continue;
+        }
+
+        final paint = Paint()..color = Color(bgColor.value);
+        canvas.drawRect(
+          Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
+          paint,
+        );
+      }
+    }
+  }
+
+  void paintSelection(Canvas canvas) {
+    for (var y = 0; y < terminal.viewHeight; y++) {
+      final offsetY = y * charSize.effectHeight;
+      final absoluteY = terminal.buffer.convertViewLineToRawLine(y) -
+          terminal.buffer.scrollOffset;
+
+      for (var x = 0; x < terminal.viewWidth; x++) {
+        var cellCount = 0;
+
+        while (
+            terminal.selection.contains(Position(x + cellCount, absoluteY)) &&
+                x + cellCount < terminal.viewWidth) {
+          cellCount++;
+        }
+
+        if (cellCount == 0) {
+          continue;
+        }
+
+        final offsetX = x * charSize.effectWidth;
+        final effectWidth = cellCount * charSize.effectWidth;
+        final effectHeight = charSize.effectHeight;
+
+        final paint = Paint()..color = Colors.white.withOpacity(0.3);
+        canvas.drawRect(
+          Rect.fromLTWH(offsetX, offsetY, effectWidth, effectHeight),
+          paint,
+        );
+
+        x += cellCount;
+      }
+    }
+  }
+
+  void paintText(Canvas canvas) {
+    final lines = terminal.getVisibleLines();
+
+    for (var i = 0; i < lines.length; i++) {
+      final line = lines[i];
+      final offsetY = i * charSize.effectHeight;
+      final cellCount = math.min(terminal.viewWidth, line.length);
+
+      for (var i = 0; i < cellCount; i++) {
+        final cell = line.getCell(i);
+
+        if (cell.attr == null || cell.width == 0) {
+          continue;
+        }
+
+        final offsetX = i * charSize.effectWidth;
+        paintCell(canvas, cell, offsetX, offsetY);
+      }
+    }
+  }
+
+  void paintTextFast(Canvas canvas) {
+    final lines = terminal.getVisibleLines();
+
+    for (var i = 0; i < lines.length; i++) {
+      final line = lines[i];
+      final offsetY = i * charSize.effectHeight;
+      final cellCount = math.min(terminal.viewWidth, line.length);
+
+      final builder = StringBuffer();
+      for (var i = 0; i < cellCount; i++) {
+        final cell = line.getCell(i);
+
+        if (cell.attr == null || cell.width == 0) {
+          continue;
+        }
+
+        if (cell.codePoint == null) {
+          builder.write(' ');
+        } else {
+          builder.writeCharCode(cell.codePoint);
+        }
+
+        // final offsetX = i * charSize.effectWidth;
+        // paintCell(canvas, cell, offsetX, offsetY);
+      }
+
+      final style = TextStyle(
+        // color: color,
+        // fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
+        // fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
+        fontSize: view.fontSize,
+        letterSpacing: charSize.letterSpacing,
+        fontFeatures: [FontFeature.tabularFigures()],
+        // decoration:
+        //     cell.attr.underline ? TextDecoration.underline : TextDecoration.none,
+        fontFamilyFallback: _kDefaultFontFamily,
+      );
+
+      final span = TextSpan(
+        text: builder.toString(),
+        style: style,
+      );
+
+      final tp = textLayoutCache.getOrPerformLayout(span);
+
+      tp.paint(canvas, Offset(0, offsetY));
+    }
+  }
+
+  void paintCell(Canvas canvas, Cell cell, double offsetX, double offsetY) {
+    if (cell.codePoint == null || cell.attr.invisible) {
+      return;
+    }
+
+    final cellColor = cell.attr.inverse
+        ? cell.attr.bgColor ?? terminal.colorScheme.background
+        : cell.attr.fgColor;
+
+    var color = Color(cellColor.value);
+
+    if (cell.attr.faint) {
+      color = color.withOpacity(0.5);
+    }
+
+    final style = TextStyle(
+      color: color,
+      fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
+      fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
+      fontSize: view.fontSize,
+      decoration:
+          cell.attr.underline ? TextDecoration.underline : TextDecoration.none,
+      fontFamilyFallback: _kDefaultFontFamily,
+    );
+
+    final span = TextSpan(
+      text: String.fromCharCode(cell.codePoint),
+      style: style,
+    );
+
+    final tp = textLayoutCache.getOrPerformLayout(span);
+
+    tp.paint(canvas, Offset(offsetX, offsetY));
+  }
+
+  void paintCursor(Canvas canvas) {
+    final screenCursorY = terminal.cursorY + terminal.scrollOffset;
+    if (screenCursorY < 0 || screenCursorY >= terminal.viewHeight) {
+      return;
+    }
+
+    final char = terminal.buffer.getCellUnderCursor();
+    final width =
+        char != null ? charSize.effectWidth * char.width : charSize.effectWidth;
+
+    final offsetX = charSize.effectWidth * terminal.cursorX;
+    final offsetY = charSize.effectHeight * screenCursorY;
+    final paint = Paint()
+      ..color = Color(terminal.colorScheme.cursor.value)
+      ..strokeWidth = focused ? 0.0 : 1.0
+      ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
+    canvas.drawRect(
+        Rect.fromLTWH(offsetX, offsetY, width, charSize.effectHeight), paint);
+  }
+
+  @override
+  bool shouldRepaint(CustomPainter oldDelegate) {
+    // print('shouldRepaint');
+    return terminal.dirty;
+  }
+}

+ 27 - 0
lib/frontend/text_layout_cache.dart

@@ -0,0 +1,27 @@
+import 'package:flutter/painting.dart';
+import 'package:quiver/collection.dart';
+
+class TextLayoutCache {
+  final LruMap<TextSpan, TextPainter> _cache;
+  final TextDirection textDirection;
+
+  TextLayoutCache(this.textDirection, int maximumSize) : _cache = LruMap<TextSpan, TextPainter>(maximumSize: maximumSize);
+
+  TextPainter getOrPerformLayout(TextSpan text) {
+    final cachedPainter = _cache[text];
+    if (cachedPainter != null) {
+      return cachedPainter;
+    } else {
+      return _performAndCacheLayout(text);
+    }
+  }
+
+  TextPainter _performAndCacheLayout(TextSpan text) {
+    final textPainter = TextPainter(text: text, textDirection: textDirection);
+    textPainter.layout();
+
+    _cache[text] = textPainter;
+
+    return textPainter;
+  }
+}

+ 835 - 0
lib/input/keys.dart

@@ -0,0 +1,835 @@
+enum TerminalKey {
+  /// Represents the logical "None" key on the keyboard.
+  none,
+
+  /// Represents the logical "Hyper" key on the keyboard.
+  hyper,
+
+  /// Represents the logical "Super Key" key on the keyboard.
+  superKey,
+
+  /// Represents the logical "Fn Lock" key on the keyboard.
+  fnLock,
+
+  /// Represents the logical "Suspend" key on the keyboard.
+  suspend,
+
+  /// Represents the logical "Resume" key on the keyboard.
+  resume,
+
+  /// Represents the logical "Turbo" key on the keyboard.
+  turbo,
+
+  /// Represents the logical "Privacy Screen Toggle" key on the keyboard.
+  privacyScreenToggle,
+
+  /// Represents the logical "Sleep" key on the keyboard.
+  sleep,
+
+  /// Represents the logical "Wake Up" key on the keyboard.
+  wakeUp,
+
+  /// Represents the logical "Display Toggle Int Ext" key on the keyboard.
+  displayToggleIntExt,
+
+  /// Represents the logical "Usb Reserved" key on the keyboard.
+  usbReserved,
+
+  /// Represents the logical "Usb Error Roll Over" key on the keyboard.
+  usbErrorRollOver,
+
+  /// Represents the logical "Usb Post Fail" key on the keyboard.
+  usbPostFail,
+
+  /// Represents the logical "Usb Error Undefined" key on the keyboard.
+  usbErrorUndefined,
+
+  /// Represents the logical "Key A" key on the keyboard.
+  keyA,
+
+  /// Represents the logical "Key B" key on the keyboard.
+  keyB,
+
+  /// Represents the logical "Key C" key on the keyboard.
+  keyC,
+
+  /// Represents the logical "Key D" key on the keyboard.
+  keyD,
+
+  /// Represents the logical "Key E" key on the keyboard.
+  keyE,
+
+  /// Represents the logical "Key F" key on the keyboard.
+  keyF,
+
+  /// Represents the logical "Key G" key on the keyboard.
+  keyG,
+
+  /// Represents the logical "Key H" key on the keyboard.
+  keyH,
+
+  /// Represents the logical "Key I" key on the keyboard.
+  keyI,
+
+  /// Represents the logical "Key J" key on the keyboard.
+  keyJ,
+
+  /// Represents the logical "Key K" key on the keyboard.
+  keyK,
+
+  /// Represents the logical "Key L" key on the keyboard.
+  keyL,
+
+  /// Represents the logical "Key M" key on the keyboard.
+  keyM,
+
+  /// Represents the logical "Key N" key on the keyboard.
+  keyN,
+
+  /// Represents the logical "Key O" key on the keyboard.
+  keyO,
+
+  /// Represents the logical "Key P" key on the keyboard.
+  keyP,
+
+  /// Represents the logical "Key Q" key on the keyboard.
+  keyQ,
+
+  /// Represents the logical "Key R" key on the keyboard.
+  keyR,
+
+  /// Represents the logical "Key S" key on the keyboard.
+  keyS,
+
+  /// Represents the logical "Key T" key on the keyboard.
+  keyT,
+
+  /// Represents the logical "Key U" key on the keyboard.
+  keyU,
+
+  /// Represents the logical "Key V" key on the keyboard.
+  keyV,
+
+  /// Represents the logical "Key W" key on the keyboard.
+  keyW,
+
+  /// Represents the logical "Key X" key on the keyboard.
+  keyX,
+
+  /// Represents the logical "Key Y" key on the keyboard.
+  keyY,
+
+  /// Represents the logical "Key Z" key on the keyboard.
+  keyZ,
+
+  /// Represents the logical "Digit 1" key on the keyboard.
+  digit1,
+
+  /// Represents the logical "Digit 2" key on the keyboard.
+  digit2,
+
+  /// Represents the logical "Digit 3" key on the keyboard.
+  digit3,
+
+  /// Represents the logical "Digit 4" key on the keyboard.
+  digit4,
+
+  /// Represents the logical "Digit 5" key on the keyboard.
+  digit5,
+
+  /// Represents the logical "Digit 6" key on the keyboard.
+  digit6,
+
+  /// Represents the logical "Digit 7" key on the keyboard.
+  digit7,
+
+  /// Represents the logical "Digit 8" key on the keyboard.
+  digit8,
+
+  /// Represents the logical "Digit 9" key on the keyboard.
+  digit9,
+
+  /// Represents the logical "Digit 0" key on the keyboard.
+  digit0,
+
+  /// Represents the logical "Enter" key on the keyboard.
+  enter,
+
+  /// Represents the logical "Escape" key on the keyboard.
+  escape,
+
+  /// Represents the logical "Backspace" key on the keyboard.
+  backspace,
+
+  /// Represents the logical "Tab" key on the keyboard.
+  tab,
+
+  /// Represents the logical "Space" key on the keyboard.
+  space,
+
+  /// Represents the logical "Minus" key on the keyboard.
+  minus,
+
+  /// Represents the logical "Equal" key on the keyboard.
+  equal,
+
+  /// Represents the logical "Bracket Left" key on the keyboard.
+  bracketLeft,
+
+  /// Represents the logical "Bracket Right" key on the keyboard.
+  bracketRight,
+
+  /// Represents the logical "Backslash" key on the keyboard.
+  backslash,
+
+  /// Represents the logical "Semicolon" key on the keyboard.
+  semicolon,
+
+  /// Represents the logical "Quote" key on the keyboard.
+  quote,
+
+  /// Represents the logical "Backquote" key on the keyboard.
+  backquote,
+
+  /// Represents the logical "Comma" key on the keyboard.
+  comma,
+
+  /// Represents the logical "Period" key on the keyboard.
+  period,
+
+  /// Represents the logical "Slash" key on the keyboard.
+  slash,
+
+  /// Represents the logical "Caps Lock" key on the keyboard.
+  capsLock,
+
+  /// Represents the logical "F1" key on the keyboard.
+  f1,
+
+  /// Represents the logical "F2" key on the keyboard.
+  f2,
+
+  /// Represents the logical "F3" key on the keyboard.
+  f3,
+
+  /// Represents the logical "F4" key on the keyboard.
+  f4,
+
+  /// Represents the logical "F5" key on the keyboard.
+  f5,
+
+  /// Represents the logical "F6" key on the keyboard.
+  f6,
+
+  /// Represents the logical "F7" key on the keyboard.
+  f7,
+
+  /// Represents the logical "F8" key on the keyboard.
+  f8,
+
+  /// Represents the logical "F9" key on the keyboard.
+  f9,
+
+  /// Represents the logical "F10" key on the keyboard.
+  f10,
+
+  /// Represents the logical "F11" key on the keyboard.
+  f11,
+
+  /// Represents the logical "F12" key on the keyboard.
+  f12,
+
+  /// Represents the logical "Print Screen" key on the keyboard.
+  printScreen,
+
+  /// Represents the logical "Scroll Lock" key on the keyboard.
+  scrollLock,
+
+  /// Represents the logical "Pause" key on the keyboard.
+  pause,
+
+  /// Represents the logical "Insert" key on the keyboard.
+  insert,
+
+  /// Represents the logical "Home" key on the keyboard.
+  home,
+
+  /// Represents the logical "Page Up" key on the keyboard.
+  pageUp,
+
+  /// Represents the logical "Delete" key on the keyboard.
+  delete,
+
+  /// Represents the logical "End" key on the keyboard.
+  end,
+
+  /// Represents the logical "Page Down" key on the keyboard.
+  pageDown,
+
+  /// Represents the logical "Arrow Right" key on the keyboard.
+  arrowRight,
+
+  /// Represents the logical "Arrow Left" key on the keyboard.
+  arrowLeft,
+
+  /// Represents the logical "Arrow Down" key on the keyboard.
+  arrowDown,
+
+  /// Represents the logical "Arrow Up" key on the keyboard.
+  arrowUp,
+
+  /// Represents the logical "Num Lock" key on the keyboard.
+  numLock,
+
+  /// Represents the logical "Numpad Divide" key on the keyboard.
+  numpadDivide,
+
+  /// Represents the logical "Numpad Multiply" key on the keyboard.
+  numpadMultiply,
+
+  /// Represents the logical "Numpad Subtract" key on the keyboard.
+  numpadSubtract,
+
+  /// Represents the logical "Numpad Add" key on the keyboard.
+  numpadAdd,
+
+  /// Represents the logical "Numpad Enter" key on the keyboard.
+  numpadEnter,
+
+  /// Represents the logical "Numpad 1" key on the keyboard.
+  numpad1,
+
+  /// Represents the logical "Numpad 2" key on the keyboard.
+  numpad2,
+
+  /// Represents the logical "Numpad 3" key on the keyboard.
+  numpad3,
+
+  /// Represents the logical "Numpad 4" key on the keyboard.
+  numpad4,
+
+  /// Represents the logical "Numpad 5" key on the keyboard.
+  numpad5,
+
+  /// Represents the logical "Numpad 6" key on the keyboard.
+  numpad6,
+
+  /// Represents the logical "Numpad 7" key on the keyboard.
+  numpad7,
+
+  /// Represents the logical "Numpad 8" key on the keyboard.
+  numpad8,
+
+  /// Represents the logical "Numpad 9" key on the keyboard.
+  numpad9,
+
+  /// Represents the logical "Numpad 0" key on the keyboard.
+  numpad0,
+
+  /// Represents the logical "Numpad Decimal" key on the keyboard.
+  numpadDecimal,
+
+  /// Represents the logical "Intl Backslash" key on the keyboard.
+  intlBackslash,
+
+  /// Represents the logical "Context Menu" key on the keyboard.
+  contextMenu,
+
+  /// Represents the logical "Power" key on the keyboard.
+  power,
+
+  /// Represents the logical "Numpad Equal" key on the keyboard.
+  numpadEqual,
+
+  /// Represents the logical "F13" key on the keyboard.
+  f13,
+
+  /// Represents the logical "F14" key on the keyboard.
+  f14,
+
+  /// Represents the logical "F15" key on the keyboard.
+  f15,
+
+  /// Represents the logical "F16" key on the keyboard.
+  f16,
+
+  /// Represents the logical "F17" key on the keyboard.
+  f17,
+
+  /// Represents the logical "F18" key on the keyboard.
+  f18,
+
+  /// Represents the logical "F19" key on the keyboard.
+  f19,
+
+  /// Represents the logical "F20" key on the keyboard.
+  f20,
+
+  /// Represents the logical "F21" key on the keyboard.
+  f21,
+
+  /// Represents the logical "F22" key on the keyboard.
+  f22,
+
+  /// Represents the logical "F23" key on the keyboard.
+  f23,
+
+  /// Represents the logical "F24" key on the keyboard.
+  f24,
+
+  /// Represents the logical "Open" key on the keyboard.
+  open,
+
+  /// Represents the logical "Help" key on the keyboard.
+  help,
+
+  /// Represents the logical "Select" key on the keyboard.
+  select,
+
+  /// Represents the logical "Again" key on the keyboard.
+  again,
+
+  /// Represents the logical "Undo" key on the keyboard.
+  undo,
+
+  /// Represents the logical "Cut" key on the keyboard.
+  cut,
+
+  /// Represents the logical "Copy" key on the keyboard.
+  copy,
+
+  /// Represents the logical "Paste" key on the keyboard.
+  paste,
+
+  /// Represents the logical "Find" key on the keyboard.
+  find,
+
+  /// Represents the logical "Audio Volume Mute" key on the keyboard.
+  audioVolumeMute,
+
+  /// Represents the logical "Audio Volume Up" key on the keyboard.
+  audioVolumeUp,
+
+  /// Represents the logical "Audio Volume Down" key on the keyboard.
+  audioVolumeDown,
+
+  /// Represents the logical "Numpad Comma" key on the keyboard.
+  numpadComma,
+
+  /// Represents the logical "Intl Ro" key on the keyboard.
+  intlRo,
+
+  /// Represents the logical "Kana Mode" key on the keyboard.
+  kanaMode,
+
+  /// Represents the logical "Intl Yen" key on the keyboard.
+  intlYen,
+
+  /// Represents the logical "Convert" key on the keyboard.
+  convert,
+
+  /// Represents the logical "Non Convert" key on the keyboard.
+  nonConvert,
+
+  /// Represents the logical "Lang 1" key on the keyboard.
+  lang1,
+
+  /// Represents the logical "Lang 2" key on the keyboard.
+  lang2,
+
+  /// Represents the logical "Lang 3" key on the keyboard.
+  lang3,
+
+  /// Represents the logical "Lang 4" key on the keyboard.
+  lang4,
+
+  /// Represents the logical "Lang 5" key on the keyboard.
+  lang5,
+
+  /// Represents the logical "Abort" key on the keyboard.
+  abort,
+
+  /// Represents the logical "Props" key on the keyboard.
+  props,
+
+  /// Represents the logical "Numpad Paren Left" key on the keyboard.
+  numpadParenLeft,
+
+  /// Represents the logical "Numpad Paren Right" key on the keyboard.
+  numpadParenRight,
+
+  /// Represents the logical "Numpad Backspace" key on the keyboard.
+  numpadBackspace,
+
+  /// Represents the logical "Numpad Memory Store" key on the keyboard.
+  numpadMemoryStore,
+
+  /// Represents the logical "Numpad Memory Recall" key on the keyboard.
+  numpadMemoryRecall,
+
+  /// Represents the logical "Numpad Memory Clear" key on the keyboard.
+  numpadMemoryClear,
+
+  /// Represents the logical "Numpad Memory Add" key on the keyboard.
+  numpadMemoryAdd,
+
+  /// Represents the logical "Numpad Memory Subtract" key on the keyboard.
+  numpadMemorySubtract,
+
+  /// Represents the logical "Numpad Sign Change" key on the keyboard.
+  numpadSignChange,
+
+  /// Represents the logical "Numpad Clear" key on the keyboard.
+  numpadClear,
+
+  /// Represents the logical "Numpad Clear Entry" key on the keyboard.
+  numpadClearEntry,
+
+  /// Represents the logical "Control Left" key on the keyboard.
+  controlLeft,
+
+  /// Represents the logical "Shift Left" key on the keyboard.
+  shiftLeft,
+
+  /// Represents the logical "Alt Left" key on the keyboard.
+  altLeft,
+
+  /// Represents the logical "Meta Left" key on the keyboard.
+  metaLeft,
+
+  /// Represents the logical "Control Right" key on the keyboard.
+  controlRight,
+
+  /// Represents the logical "Shift Right" key on the keyboard.
+  shiftRight,
+
+  /// Represents the logical "Alt Right" key on the keyboard.
+  altRight,
+
+  /// Represents the logical "Meta Right" key on the keyboard.
+  metaRight,
+
+  /// Represents the logical "Info" key on the keyboard.
+  info,
+
+  /// Represents the logical "Closed Caption Toggle" key on the keyboard.
+  closedCaptionToggle,
+
+  /// Represents the logical "Brightness Up" key on the keyboard.
+  brightnessUp,
+
+  /// Represents the logical "Brightness Down" key on the keyboard.
+  brightnessDown,
+
+  /// Represents the logical "Brightness Toggle" key on the keyboard.
+  brightnessToggle,
+
+  /// Represents the logical "Brightness Minimum" key on the keyboard.
+  brightnessMinimum,
+
+  /// Represents the logical "Brightness Maximum" key on the keyboard.
+  brightnessMaximum,
+
+  /// Represents the logical "Brightness Auto" key on the keyboard.
+  brightnessAuto,
+
+  /// Represents the logical "Media Last" key on the keyboard.
+  mediaLast,
+
+  /// Represents the logical "Launch Phone" key on the keyboard.
+  launchPhone,
+
+  /// Represents the logical "Program Guide" key on the keyboard.
+  programGuide,
+
+  /// Represents the logical "Exit" key on the keyboard.
+  exit,
+
+  /// Represents the logical "Channel Up" key on the keyboard.
+  channelUp,
+
+  /// Represents the logical "Channel Down" key on the keyboard.
+  channelDown,
+
+  /// Represents the logical "Media Play" key on the keyboard.
+  mediaPlay,
+
+  /// Represents the logical "Media Pause" key on the keyboard.
+  mediaPause,
+
+  /// Represents the logical "Media Record" key on the keyboard.
+  mediaRecord,
+
+  /// Represents the logical "Media Fast Forward" key on the keyboard.
+  mediaFastForward,
+
+  /// Represents the logical "Media Rewind" key on the keyboard.
+  mediaRewind,
+
+  /// Represents the logical "Media Track Next" key on the keyboard.
+  mediaTrackNext,
+
+  /// Represents the logical "Media Track Previous" key on the keyboard.
+  mediaTrackPrevious,
+
+  /// Represents the logical "Media Stop" key on the keyboard.
+  mediaStop,
+
+  /// Represents the logical "Eject" key on the keyboard.
+  eject,
+
+  /// Represents the logical "Media Play Pause" key on the keyboard.
+  mediaPlayPause,
+
+  /// Represents the logical "Speech Input Toggle" key on the keyboard.
+  speechInputToggle,
+
+  /// Represents the logical "Bass Boost" key on the keyboard.
+  bassBoost,
+
+  /// Represents the logical "Media Select" key on the keyboard.
+  mediaSelect,
+
+  /// Represents the logical "Launch Word Processor" key on the keyboard.
+  launchWordProcessor,
+
+  /// Represents the logical "Launch Spreadsheet" key on the keyboard.
+  launchSpreadsheet,
+
+  /// Represents the logical "Launch Mail" key on the keyboard.
+  launchMail,
+
+  /// Represents the logical "Launch Contacts" key on the keyboard.
+  launchContacts,
+
+  /// Represents the logical "Launch Calendar" key on the keyboard.
+  launchCalendar,
+
+  /// Represents the logical "Launch App2" key on the keyboard.
+  launchApp2,
+
+  /// Represents the logical "Launch App1" key on the keyboard.
+  launchApp1,
+
+  /// Represents the logical "Launch Internet Browser" key on the keyboard.
+  launchInternetBrowser,
+
+  /// Represents the logical "Log Off" key on the keyboard.
+  logOff,
+
+  /// Represents the logical "Lock Screen" key on the keyboard.
+  lockScreen,
+
+  /// Represents the logical "Launch Control Panel" key on the keyboard.
+  launchControlPanel,
+
+  /// Represents the logical "Select Task" key on the keyboard.
+  selectTask,
+
+  /// Represents the logical "Launch Documents" key on the keyboard.
+  launchDocuments,
+
+  /// Represents the logical "Spell Check" key on the keyboard.
+  spellCheck,
+
+  /// Represents the logical "Launch Keyboard Layout" key on the keyboard.
+  launchKeyboardLayout,
+
+  /// Represents the logical "Launch Screen Saver" key on the keyboard.
+  launchScreenSaver,
+
+  /// Represents the logical "Launch Assistant" key on the keyboard.
+  launchAssistant,
+
+  /// Represents the logical "Launch Audio Browser" key on the keyboard.
+  launchAudioBrowser,
+
+  /// Represents the logical "New Key" key on the keyboard.
+  newKey,
+
+  /// Represents the logical "Close" key on the keyboard.
+  close,
+
+  /// Represents the logical "Save" key on the keyboard.
+  save,
+
+  /// Represents the logical "Print" key on the keyboard.
+  print,
+
+  /// Represents the logical "Browser Search" key on the keyboard.
+  browserSearch,
+
+  /// Represents the logical "Browser Home" key on the keyboard.
+  browserHome,
+
+  /// Represents the logical "Browser Back" key on the keyboard.
+  browserBack,
+
+  /// Represents the logical "Browser Forward" key on the keyboard.
+  browserForward,
+
+  /// Represents the logical "Browser Stop" key on the keyboard.
+  browserStop,
+
+  /// Represents the logical "Browser Refresh" key on the keyboard.
+  browserRefresh,
+
+  /// Represents the logical "Browser Favorites" key on the keyboard.
+  browserFavorites,
+
+  /// Represents the logical "Zoom In" key on the keyboard.
+  zoomIn,
+
+  /// Represents the logical "Zoom Out" key on the keyboard.
+  zoomOut,
+
+  /// Represents the logical "Zoom Toggle" key on the keyboard.
+  zoomToggle,
+
+  /// Represents the logical "Redo" key on the keyboard.
+  redo,
+
+  /// Represents the logical "Mail Reply" key on the keyboard.
+  mailReply,
+
+  /// Represents the logical "Mail Forward" key on the keyboard.
+  mailForward,
+
+  /// Represents the logical "Mail Send" key on the keyboard.
+  mailSend,
+
+  /// Represents the logical "Keyboard Layout Select" key on the keyboard.
+  keyboardLayoutSelect,
+
+  /// Represents the logical "Show All Windows" key on the keyboard.
+  showAllWindows,
+
+  /// Represents the logical "Game Button 1" key on the keyboard.
+  gameButton1,
+
+  /// Represents the logical "Game Button 2" key on the keyboard.
+  gameButton2,
+
+  /// Represents the logical "Game Button 3" key on the keyboard.
+  gameButton3,
+
+  /// Represents the logical "Game Button 4" key on the keyboard.
+  gameButton4,
+
+  /// Represents the logical "Game Button 5" key on the keyboard.
+  gameButton5,
+
+  /// Represents the logical "Game Button 6" key on the keyboard.
+  gameButton6,
+
+  /// Represents the logical "Game Button 7" key on the keyboard.
+  gameButton7,
+
+  /// Represents the logical "Game Button 8" key on the keyboard.
+  gameButton8,
+
+  /// Represents the logical "Game Button 9" key on the keyboard.
+  gameButton9,
+
+  /// Represents the logical "Game Button 10" key on the keyboard.
+  gameButton10,
+
+  /// Represents the logical "Game Button 11" key on the keyboard.
+  gameButton11,
+
+  /// Represents the logical "Game Button 12" key on the keyboard.
+  gameButton12,
+
+  /// Represents the logical "Game Button 13" key on the keyboard.
+  gameButton13,
+
+  /// Represents the logical "Game Button 14" key on the keyboard.
+  gameButton14,
+
+  /// Represents the logical "Game Button 15" key on the keyboard.
+  gameButton15,
+
+  /// Represents the logical "Game Button 16" key on the keyboard.
+  gameButton16,
+
+  /// Represents the logical "Game Button A" key on the keyboard.
+  gameButtonA,
+
+  /// Represents the logical "Game Button B" key on the keyboard.
+  gameButtonB,
+
+  /// Represents the logical "Game Button C" key on the keyboard.
+  gameButtonC,
+
+  /// Represents the logical "Game Button Left 1" key on the keyboard.
+  gameButtonLeft1,
+
+  /// Represents the logical "Game Button Left 2" key on the keyboard.
+  gameButtonLeft2,
+
+  /// Represents the logical "Game Button Mode" key on the keyboard.
+  gameButtonMode,
+
+  /// Represents the logical "Game Button Right 1" key on the keyboard.
+  gameButtonRight1,
+
+  /// Represents the logical "Game Button Right 2" key on the keyboard.
+  gameButtonRight2,
+
+  /// Represents the logical "Game Button Select" key on the keyboard.
+  gameButtonSelect,
+
+  /// Represents the logical "Game Button Start" key on the keyboard.
+  gameButtonStart,
+
+  /// Represents the logical "Game Button Thumb Left" key on the keyboard.
+  gameButtonThumbLeft,
+
+  /// Represents the logical "Game Button Thumb Right" key on the keyboard.
+  gameButtonThumbRight,
+
+  /// Represents the logical "Game Button X" key on the keyboard.
+  gameButtonX,
+
+  /// Represents the logical "Game Button Y" key on the keyboard.
+  gameButtonY,
+
+  /// Represents the logical "Game Button Z" key on the keyboard.
+  gameButtonZ,
+
+  /// Represents the logical "Fn" key on the keyboard.
+  fn,
+
+  /// Represents the logical "Shift" key on the keyboard.
+  ///
+  /// This key represents the union of the keys {shiftLeft, shiftRight} when
+  /// comparing keys. This key will never be generated directly, its main use is
+  /// in defining key maps.
+  shift,
+
+  /// Represents the logical "Meta" key on the keyboard.
+  ///
+  /// This key represents the union of the keys {metaLeft, metaRight} when
+  /// comparing keys. This key will never be generated directly, its main use is
+  /// in defining key maps.
+  meta,
+
+  /// Represents the logical "Alt" key on the keyboard.
+  ///
+  /// This key represents the union of the keys {altLeft, altRight} when
+  /// comparing keys. This key will never be generated directly, its main use is
+  /// in defining key maps.
+  alt,
+
+  /// Represents the logical "Control" key on the keyboard.
+  ///
+  /// This key represents the union of the keys {controlLeft, controlRight} when
+  /// comparing keys. This key will never be generated directly, its main use is
+  /// in defining key maps.
+  control,
+
+  // Missing flutter keys.
+
+  backtab,
+  returnKey,
+}

+ 38 - 0
lib/input/keytab/keytab.dart

@@ -0,0 +1,38 @@
+import 'package:meta/meta.dart';
+import 'package:xterm/input/keytab/keytab_default.dart';
+import 'package:xterm/input/keytab/keytab_parse.dart';
+import 'package:xterm/input/keytab/keytab_record.dart';
+import 'package:xterm/input/keytab/keytab_token.dart';
+
+class Keytab {
+  Keytab({
+    @required this.name,
+    @required this.records,
+  });
+
+  factory Keytab.parse(String source) {
+    final tokens = tokenize(source).toList();
+    final parser = KeytabParser()..addTokens(tokens);
+    return parser.result;
+  }
+
+  factory Keytab.defaultKeytab() {
+    return Keytab.parse(kDefaultKeytab);
+  }
+
+  final String name;
+  final List<KeytabRecord> records;
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+
+    buffer.writeln('keyboard "$name"');
+
+    for (var record in records) {
+      buffer.writeln(record);
+    }
+
+    return buffer.toString();
+  }
+}

+ 200 - 0
lib/input/keytab/keytab_default.dart

@@ -0,0 +1,200 @@
+import 'package:xterm/input/keytab/keytab_parse.dart';
+import 'package:xterm/input/keytab/keytab_token.dart';
+
+const kDefaultKeytab = r'''
+# [README.default.Keytab] Default Keyboard Table
+#
+# To customize your keyboard, copy this file to something ending with
+# .keytab and change it to meet you needs.
+#
+# Please read the README-KeyTab and the doc/user/README.keyboard files
+# in this case.
+#
+# --------------------------------------------------------------
+
+keyboard "Default (XFree 4)"
+
+# --------------------------------------------------------------
+#
+# Note that this particular table is a "risc" version made to
+# ease customization without bothering with obsolete details.
+# See VT100.keytab for the more hairy stuff.
+#
+# --------------------------------------------------------------
+
+# common keys
+
+key Escape             : "\E"
+
+key Tab   -Shift       : "\t"
+key Tab   +Shift+Ansi  : "\E[Z"
+key Tab   +Shift-Ansi  : "\t"
+key Backtab     +Ansi  : "\E[Z"
+key Backtab     -Ansi  : "\t"
+
+key Return-Shift-NewLine : "\r"
+key Return-Shift+NewLine : "\r\n"
+
+key Return+Shift         : "\EOM"
+
+key Backspace  +Alt : "\x17"
+
+# Backspace and Delete codes are preserving CTRL-H.
+#
+# Backspace without CTRL sends '^?'; this matches XTerm behaviour, so that
+# pressing Alt+Backspace will send \E + Del, which is the expected behaviour
+# in some apps (e.g. emacs), and it was the behaviour before the commit
+# that add the Backspace +Control rule
+key Backspace   -Control : "\x7f"
+
+# Match xterm behaviour: Backspace sends '^H' when Control is pressed
+# BS, hex \x08, \b
+key Backspace  +Control : "\b"
+
+# Arrow keys in VT52 mode
+# shift up/down are reserved for scrolling.
+# shift left/right are reserved for switching between tabs (this is hardcoded).
+
+key Up   -Shift-Ansi : "\EA"
+key Down -Shift-Ansi : "\EB"
+key Right-Shift-Ansi : "\EC"
+key Left -Shift-Ansi : "\ED"
+
+# Arrow keys in ANSI mode with Application - and Normal Cursor Mode)
+
+key Up    -Shift-AnyMod+Ansi+AppCuKeys : "\EOA"
+key Down  -Shift-AnyMod+Ansi+AppCuKeys : "\EOB"
+key Right -Shift-AnyMod+Ansi+AppCuKeys : "\EOC"
+key Left  -Shift-AnyMod+Ansi+AppCuKeys : "\EOD"
+
+key Up    -Shift-AnyMod+Ansi-AppCuKeys : "\E[A"
+key Down  -Shift-AnyMod+Ansi-AppCuKeys : "\E[B"
+key Right -Shift-AnyMod+Ansi-AppCuKeys : "\E[C"
+key Left  -Shift-AnyMod+Ansi-AppCuKeys : "\E[D"
+
+key Up    -Shift+AnyMod+Ansi           : "\E[1;5A"
+key Down  -Shift+AnyMod+Ansi           : "\E[1;5B"
+key Right -Shift+AnyMod+Ansi           : "\E[1;5C"
+key Left  -Shift+AnyMod+Ansi           : "\E[1;5D"
+
+key Up    +Shift+AppScreen             : "\E[1;*A"
+key Down  +Shift+AppScreen             : "\E[1;*B"
+key Left  +Shift+AppScreen             : "\E[1;*D"
+key Right +Shift+AppScreen             : "\E[1;*C"
+
+# Keypad keys with NumLock ON
+# (see https://web.archive.org/web/20070807181942/http://www.nw.com/nw/WWW/products/wizcon/vt100.html
+#    https://vt100.net/docs/vt100-ug/chapter3.html)
+#
+# Not enabled for now because it breaks the keypad in Vim.
+#
+#key 0 +KeyPad+AppKeyPad : "\EOp"
+#key 1 +KeyPad+AppKeyPad : "\EOq"
+#key 2 +KeyPad+AppKeyPad : "\EOr"
+#key 3 +KeyPad+AppKeyPad : "\EOs"
+#key 4 +KeyPad+AppKeyPad : "\EOt"
+#key 5 +KeyPad+AppKeyPad : "\EOu"
+#key 6 +KeyPad+AppKeyPad : "\EOv"
+#key 7 +KeyPad+AppKeyPad : "\EOw"
+#key 8 +KeyPad+AppKeyPad : "\EOx"
+#key 9 +KeyPad+AppKeyPad : "\EOy"
+#key + +KeyPad+AppKeyPad : "\EOl"
+#key - +KeyPad+AppKeyPad : "\EOm"
+#key . +KeyPad+AppKeyPad : "\EOn"
+#key * +KeyPad+AppKeyPad : "\EOM"
+#key Enter +KeyPad+AppKeyPad : "\r"
+
+# Keypad keys with NumLock Off
+key Up    -Shift+Ansi+AppCuKeys+KeyPad : "\EOA"
+key Down  -Shift+Ansi+AppCuKeys+KeyPad : "\EOB"
+key Right -Shift+Ansi+AppCuKeys+KeyPad : "\EOC"
+key Left  -Shift+Ansi+AppCuKeys+KeyPad : "\EOD"
+
+key Up    -Shift+Ansi-AppCuKeys+KeyPad : "\E[A"
+key Down  -Shift+Ansi-AppCuKeys+KeyPad : "\E[B"
+key Right -Shift+Ansi-AppCuKeys+KeyPad : "\E[C"
+key Left  -Shift+Ansi-AppCuKeys+KeyPad : "\E[D"
+
+key Home        +AppCuKeys+KeyPad : "\EOH"
+key End         +AppCuKeys+KeyPad : "\EOF"
+key Home        -AppCuKeys+KeyPad : "\E[H"
+key End         -AppCuKeys+KeyPad : "\E[F"
+
+key Insert      +KeyPad  : "\E[2~"
+key Delete      +KeyPad  : "\E[3~"
+key PgUp -Shift+KeyPad  : "\E[5~"
+key PgDown  -Shift+KeyPad  : "\E[6~"
+
+# the key labelled 5 on the Keypad, is Qt::Key_Clear (a very intuitive
+# and discoverable name...)
+key Clear       +KeyPad : "\E[E"
+
+# other grey PC keys
+
+key Enter+NewLine : "\r\n"
+key Enter-NewLine : "\r"
+
+key Home        -AnyMod-AppCuKeys : "\E[H"  
+key End         -AnyMod-AppCuKeys : "\E[F"  
+key Home        -AnyMod+AppCuKeys : "\EOH"  
+key End         -AnyMod+AppCuKeys : "\EOF"  
+key Home        +AnyMod           : "\E[1;*H"
+key End         +AnyMod           : "\E[1;*F"
+
+key Insert      -AnyMod  : "\E[2~"
+key Delete      -AnyMod  : "\E[3~"
+key Insert      +AnyMod  : "\E[2;*~"
+key Delete      +AnyMod  : "\E[3;*~"
+
+key PgUp -Shift-AnyMod  : "\E[5~"
+key PgDown  -Shift-AnyMod  : "\E[6~"
+key PgUp -Shift+AnyMod  : "\E[5;*~"
+key PgDown  -Shift+AnyMod  : "\E[6;*~"
+
+# Function keys
+key F1  -AnyMod  : "\EOP"
+key F2  -AnyMod  : "\EOQ"
+key F3  -AnyMod  : "\EOR"
+key F4  -AnyMod  : "\EOS"
+key F5  -AnyMod  : "\E[15~"
+key F6  -AnyMod  : "\E[17~"
+key F7  -AnyMod  : "\E[18~"
+key F8  -AnyMod  : "\E[19~"
+key F9  -AnyMod  : "\E[20~"
+key F10 -AnyMod  : "\E[21~"
+key F11 -AnyMod  : "\E[23~"
+key F12 -AnyMod  : "\E[24~"
+
+key F1  +AnyMod  : "\EO*P"
+key F2  +AnyMod  : "\EO*Q"
+key F3  +AnyMod  : "\EO*R"
+key F4  +AnyMod  : "\EO*S"
+key F5  +AnyMod  : "\E[15;*~"
+key F6  +AnyMod  : "\E[17;*~"
+key F7  +AnyMod  : "\E[18;*~"
+key F8  +AnyMod  : "\E[19;*~"
+key F9  +AnyMod  : "\E[20;*~"
+key F10 +AnyMod  : "\E[21;*~"
+key F11 +AnyMod  : "\E[23;*~"
+key F12 +AnyMod  : "\E[24;*~"
+
+# Work around dead keys
+
+key Space +Control : "\x00"
+
+# Some keys are used by konsole to cause operations.
+# The scroll* operations refer to the history buffer.
+
+key Up    +Shift-AppScreen  : scrollLineUp
+key PgUp  +Shift-AppScreen  : scrollPageUp
+key Home  +Shift-AppScreen  : scrollUpToTop
+key Down  +Shift-AppScreen  : scrollLineDown
+key PgDown  +Shift-AppScreen  : scrollPageDown
+key End   +Shift-AppScreen  : scrollDownToBottom
+''';
+
+void main() {
+  final tokens = tokenize(kDefaultKeytab).toList();
+  final parser = KeytabParser()..addTokens(tokens);
+  print(parser.result);
+}

+ 21 - 0
lib/input/keytab/keytab_escape.dart

@@ -0,0 +1,21 @@
+final _esc = String.fromCharCode(0x1b);
+
+String keytabUnescape(String str) {
+  str = str
+      .replaceAll(r'\E', _esc)
+      .replaceAll(r'\\', '\\')
+      .replaceAll(r'\"', '\"')
+      .replaceAll(r'\t', '\t')
+      .replaceAll(r'\r', '\r')
+      .replaceAll(r'\n', '\n')
+      .replaceAll(r'\b', '\b');
+
+  final hexPattern = RegExp(r'\\x([0-9a-fA-F][0-9a-fA-F])');
+  str = str.replaceAllMapped(hexPattern, (match) {
+    final hexString = match.group(1);
+    final hexValue = int.parse(hexString, radix: 16);
+    return String.fromCharCode(hexValue);
+  });
+
+  return str;
+}

+ 181 - 0
lib/input/keytab/keytab_parse.dart

@@ -0,0 +1,181 @@
+import 'package:xterm/input/keytab/keytab.dart';
+import 'package:xterm/input/keytab/keytab_record.dart';
+import 'package:xterm/input/keytab/keytab_token.dart';
+import 'package:xterm/input/keytab/qt_keyname.dart';
+
+class ParseError {}
+
+class TokensReader {
+  TokensReader(this.tokens);
+
+  final List<KeytabToken> tokens;
+
+  var _pos = 0;
+
+  bool get done => _pos > tokens.length - 1;
+
+  KeytabToken take() {
+    final result = peek();
+    _pos += 1;
+    return result;
+  }
+
+  KeytabToken peek() {
+    if (done) return null;
+    return tokens[_pos];
+  }
+}
+
+class KeytabParser {
+  String _name;
+  final _records = <KeytabRecord>[];
+
+  void addTokens(List<KeytabToken> tokens) {
+    final reader = TokensReader(tokens);
+
+    while (!reader.done) {
+      if (reader.peek().type == KeytabTokenType.keyboard) {
+        _parseName(reader);
+        continue;
+      }
+
+      if (reader.peek().type == KeytabTokenType.keyDefine) {
+        _parseKeyDefine(reader);
+        continue;
+      }
+
+      throw ParseError();
+    }
+  }
+
+  Keytab get result {
+    return Keytab(name: _name, records: _records);
+  }
+
+  void _parseName(TokensReader reader) {
+    if (reader.take().type != KeytabTokenType.keyboard) {
+      throw ParseError();
+    }
+
+    final name = reader.take();
+    if (name.type != KeytabTokenType.input) {
+      throw ParseError();
+    }
+
+    _name = name.value;
+  }
+
+  void _parseKeyDefine(TokensReader reader) {
+    if (reader.take().type != KeytabTokenType.keyDefine) {
+      throw ParseError();
+    }
+
+    final keyName = reader.take();
+
+    if (keyName.type != KeytabTokenType.keyName) {
+      throw ParseError();
+    }
+
+    final key = qtKeynameMap[keyName.value];
+    if (key == null) {
+      throw ParseError();
+    }
+
+    bool alt;
+    bool ctrl;
+    bool shift;
+    bool anyModifier;
+    bool ansi;
+    bool appScreen;
+    bool keyPad;
+    bool appCursorKeys;
+    bool appKeyPad;
+    bool newLine;
+
+    while (reader.peek().type == KeytabTokenType.modeStatus) {
+      bool modeStatus;
+      switch (reader.take().value) {
+        case '+':
+          modeStatus = true;
+          break;
+        case '-':
+          modeStatus = false;
+          break;
+        default:
+          throw ParseError();
+      }
+
+      final mode = reader.take();
+      if (mode.type != KeytabTokenType.mode) {
+        throw ParseError();
+      }
+
+      switch (mode.value) {
+        case 'Alt':
+          alt = modeStatus;
+          break;
+        case 'Control':
+          ctrl = modeStatus;
+          break;
+        case 'Shift':
+          shift = modeStatus;
+          break;
+        case 'AnyMod':
+          anyModifier = modeStatus;
+          break;
+        case 'Ansi':
+          ansi = modeStatus;
+          break;
+        case 'AppScreen':
+          appScreen = modeStatus;
+          break;
+        case 'KeyPad':
+          keyPad = modeStatus;
+          break;
+        case 'AppCuKeys':
+          appCursorKeys = modeStatus;
+          break;
+        case 'AppKeyPad':
+          appKeyPad = modeStatus;
+          break;
+        case 'NewLine':
+          newLine = modeStatus;
+          break;
+        default:
+          throw ParseError();
+      }
+    }
+
+    if (reader.take().type != KeytabTokenType.colon) {
+      throw ParseError();
+    }
+
+    final actionToken = reader.take();
+    KeytabAction action;
+    if (actionToken.type == KeytabTokenType.input) {
+      action = KeytabAction(KeytabActionType.input, actionToken.value);
+    } else if (actionToken.type == KeytabTokenType.shortcut) {
+      action = KeytabAction(KeytabActionType.shortcut, actionToken.value);
+    } else {
+      throw ParseError();
+    }
+
+    final record = KeytabRecord(
+      qtKeyName: keyName.value,
+      key: key,
+      action: action,
+      alt: alt,
+      ctrl: ctrl,
+      shift: shift,
+      anyModifier: anyModifier,
+      ansi: ansi,
+      appScreen: appScreen,
+      keyPad: keyPad,
+      appCursorKeys: appCursorKeys,
+      appKeyPad: appKeyPad,
+      newLine: newLine,
+    );
+
+    _records.add(record);
+  }
+}

+ 121 - 0
lib/input/keytab/keytab_record.dart

@@ -0,0 +1,121 @@
+import 'package:meta/meta.dart';
+import 'package:xterm/input/keys.dart';
+
+enum KeytabActionType {
+  input,
+  shortcut,
+}
+
+class KeytabAction {
+  KeytabAction(this.type, this.value);
+
+  final KeytabActionType type;
+  final String value;
+
+  @override
+  String toString() {
+    switch (type) {
+      case KeytabActionType.input:
+        return '"$value"';
+      case KeytabActionType.shortcut:
+        return value;
+      default:
+        return '(no value)';
+    }
+  }
+}
+
+class KeytabRecord {
+  KeytabRecord({
+    @required this.qtKeyName,
+    @required this.key,
+    @required this.action,
+    @required this.alt,
+    @required this.ctrl,
+    @required this.shift,
+    @required this.anyModifier,
+    @required this.ansi,
+    @required this.appScreen,
+    @required this.keyPad,
+    @required this.appCursorKeys,
+    @required this.appKeyPad,
+    @required this.newLine,
+  });
+
+  String qtKeyName;
+  TerminalKey key;
+  KeytabAction action;
+
+  bool alt;
+  bool ctrl;
+  bool shift;
+  bool anyModifier;
+  bool ansi;
+  bool appScreen;
+  bool keyPad;
+  bool appCursorKeys;
+  bool appKeyPad;
+  bool newLine;
+
+  @override
+  String toString() {
+    final buffer = StringBuffer();
+    buffer.write('$qtKeyName ');
+
+    if (alt != null) {
+      buffer.write(modeStatus(alt, 'Alt'));
+    }
+
+    if (ctrl != null) {
+      buffer.write(modeStatus(ctrl, 'Control'));
+    }
+
+    if (shift != null) {
+      buffer.write(modeStatus(shift, 'Shift'));
+    }
+
+    if (anyModifier != null) {
+      buffer.write(modeStatus(anyModifier, 'AnyMod'));
+    }
+
+    if (ansi != null) {
+      buffer.write(modeStatus(ansi, 'Ansi'));
+    }
+
+    if (appScreen != null) {
+      buffer.write(modeStatus(appScreen, 'AppScreen'));
+    }
+
+    if (keyPad != null) {
+      buffer.write(modeStatus(keyPad, 'KeyPad'));
+    }
+
+    if (appCursorKeys != null) {
+      buffer.write(modeStatus(appCursorKeys, 'AppCuKeys'));
+    }
+
+    if (appKeyPad != null) {
+      buffer.write(modeStatus(appKeyPad, 'AppKeyPad'));
+    }
+
+    if (newLine != null) {
+      buffer.write(modeStatus(newLine, 'NewLine'));
+    }
+
+    buffer.write(' : $action');
+
+    return buffer.toString();
+  }
+}
+
+String modeStatus(bool status, String mode) {
+  if (status == true) {
+    return '+$mode';
+  }
+
+  if (status == false) {
+    return '-$mode';
+  }
+
+  return '';
+}

+ 179 - 0
lib/input/keytab/keytab_token.dart

@@ -0,0 +1,179 @@
+import 'dart:math' show min;
+
+enum KeytabTokenType {
+  keyDefine,
+  keyboard,
+  keyName,
+  mode,
+  modeStatus,
+  colon,
+  input,
+  shortcut,
+}
+
+class KeytabToken {
+  KeytabToken(this.type, this.value);
+
+  final KeytabTokenType type;
+  final String value;
+
+  @override
+  String toString() {
+    return '$type<$value>';
+  }
+}
+
+class LineReader {
+  LineReader(this.line);
+
+  final String line;
+
+  var _pos = 0;
+
+  bool get done => _pos > line.length - 1;
+
+  String take([int count = 1]) {
+    final result = peek(count);
+    _pos += count;
+    return result;
+  }
+
+  String peek([int count = 1]) {
+    if (done) return null;
+    final end = min(_pos + count, line.length);
+    final result = line.substring(_pos, end);
+    return result;
+  }
+
+  void skipWhitespace() {
+    while (peek() == ' ' || peek() == '\t') {
+      _pos += 1;
+    }
+  }
+
+  String readString() {
+    final buffer = StringBuffer();
+    final pattern = RegExp(r'\w|_');
+
+    while (!done && line[_pos].contains(pattern)) {
+      buffer.write(line[_pos]);
+      _pos++;
+    }
+
+    return buffer.toString();
+  }
+
+  String readUntil(Pattern pattern, {bool inclusive = false}) {
+    final buffer = StringBuffer();
+
+    while (!done && !line[_pos].contains(pattern)) {
+      buffer.write(line[_pos]);
+      _pos++;
+    }
+
+    if (!done && inclusive) {
+      buffer.write(line[_pos]);
+      _pos++;
+    }
+
+    return buffer.toString();
+  }
+}
+
+class TokenizeError {}
+
+Iterable<KeytabToken> tokenize(String source) sync* {
+  final lines = source.split('\n');
+
+  for (var i = 0; i < lines.length; i++) {
+    var line = lines[i].trim();
+    line = line.replaceFirst(RegExp('#.*'), '');
+
+    if (line == '') {
+      continue;
+    }
+
+    if (_isKeyboardNameDefine(line)) {
+      yield* _parseKeyboardNameDefine(line);
+    }
+
+    if (_isKeyDefine(line)) {
+      yield* _parseKeyDefine(line);
+    }
+  }
+}
+
+bool _isKeyboardNameDefine(String line) {
+  return line.startsWith('keyboard ');
+}
+
+bool _isKeyDefine(String line) {
+  return line.startsWith('key ');
+}
+
+Iterable<KeytabToken> _parseKeyboardNameDefine(String line) sync* {
+  final reader = LineReader(line.trim());
+
+  if (reader.readString() == 'keyboard') {
+    yield KeytabToken(KeytabTokenType.keyboard, 'keyboard');
+  } else {
+    throw TokenizeError();
+  }
+
+  reader.skipWhitespace();
+
+  yield _readInput(reader);
+}
+
+Iterable<KeytabToken> _parseKeyDefine(String line) sync* {
+  final reader = LineReader(line.trim());
+
+  if (reader.readString() == 'key') {
+    yield KeytabToken(KeytabTokenType.keyDefine, 'key');
+  } else {
+    throw TokenizeError();
+  }
+
+  reader.skipWhitespace();
+
+  final keyName = reader.readString();
+  yield KeytabToken(KeytabTokenType.keyName, keyName);
+
+  reader.skipWhitespace();
+
+  while (reader.peek() == '+' || reader.peek() == '-') {
+    final modeStatus = reader.take();
+    yield KeytabToken(KeytabTokenType.modeStatus, modeStatus);
+    final mode = reader.readString();
+    yield KeytabToken(KeytabTokenType.mode, mode);
+    reader.skipWhitespace();
+  }
+
+  if (reader.take() == ':') {
+    yield KeytabToken(KeytabTokenType.colon, ':');
+  } else {
+    throw TokenizeError();
+  }
+
+  reader.skipWhitespace();
+
+  if (reader.peek() == '"') {
+    yield _readInput(reader);
+  } else {
+    final action = reader.readString();
+    yield KeytabToken(KeytabTokenType.shortcut, action);
+  }
+}
+
+KeytabToken _readInput(LineReader reader) {
+  reader.skipWhitespace();
+
+  if (reader.take() != '"') {
+    throw TokenizeError();
+  }
+
+  final value = reader.readUntil('"');
+  reader.take();
+
+  return KeytabToken(KeytabTokenType.input, value);
+}

+ 476 - 0
lib/input/keytab/qt_keyname.dart

@@ -0,0 +1,476 @@
+import 'package:xterm/input/keys.dart';
+
+/// See: https://doc.qt.io/qt-5/qt.html#Key-enum
+const qtKeynameMap = <String, TerminalKey>{
+  'Escape': TerminalKey.escape,
+  'Tab': TerminalKey.tab,
+  'Backtab': TerminalKey.backtab,
+  'Backspace': TerminalKey.backspace,
+  'Return': TerminalKey.returnKey,
+  'Enter': TerminalKey.enter,
+  'Insert': TerminalKey.insert,
+  'Delete': TerminalKey.delete,
+  'Pause': TerminalKey.pause,
+  'Print': TerminalKey.print,
+// 'SysReq': TerminalKey.sysReq,
+  'Clear': TerminalKey.numpadClear,
+  'Home': TerminalKey.home,
+  'End': TerminalKey.end,
+  'Left': TerminalKey.arrowLeft,
+  'Up': TerminalKey.arrowUp,
+  'Right': TerminalKey.arrowRight,
+  'Down': TerminalKey.arrowDown,
+  'PageUp': TerminalKey.pageUp,
+  'PageDown': TerminalKey.pageDown,
+  'PgUp': TerminalKey.pageUp,
+  'PgDown': TerminalKey.pageDown,
+  'Shift': TerminalKey.shift,
+  'Control': TerminalKey.control,
+  'Meta': TerminalKey.meta,
+  'Alt': TerminalKey.alt,
+// 'AltGr': TerminalKey.altGr,
+  'CapsLock': TerminalKey.capsLock,
+  'NumLock': TerminalKey.numLock,
+  'ScrollLock': TerminalKey.scrollLock,
+  'F1': TerminalKey.f1,
+  'F2': TerminalKey.f2,
+  'F3': TerminalKey.f3,
+  'F4': TerminalKey.f4,
+  'F5': TerminalKey.f5,
+  'F6': TerminalKey.f6,
+  'F7': TerminalKey.f7,
+  'F8': TerminalKey.f8,
+  'F9': TerminalKey.f9,
+  'F10': TerminalKey.f10,
+  'F11': TerminalKey.f11,
+  'F12': TerminalKey.f12,
+  'F13': TerminalKey.f13,
+  'F14': TerminalKey.f14,
+  'F15': TerminalKey.f15,
+  'F16': TerminalKey.f16,
+  'F17': TerminalKey.f17,
+  'F18': TerminalKey.f18,
+  'F19': TerminalKey.f19,
+  'F20': TerminalKey.f20,
+  'F21': TerminalKey.f21,
+  'F22': TerminalKey.f22,
+  'F23': TerminalKey.f23,
+  'F24': TerminalKey.f24,
+// 'F25': TerminalKey.f25,
+// 'F26': TerminalKey.f26,
+// 'F27': TerminalKey.f27,
+// 'F28': TerminalKey.f28,
+// 'F29': TerminalKey.f29,
+// 'F30': TerminalKey.f30,
+// 'F31': TerminalKey.f31,
+// 'F32': TerminalKey.f32,
+// 'F33': TerminalKey.f33,
+// 'F34': TerminalKey.f34,
+// 'F35': TerminalKey.f35,
+// 'Super_L': TerminalKey.super_L,
+// 'Super_R': TerminalKey.super_R,
+// 'Menu': TerminalKey.menu,
+// 'Hyper_L': TerminalKey.hyper_L,
+// 'Hyper_R': TerminalKey.hyper_R,
+  'Help': TerminalKey.help,
+// 'Direction_L': TerminalKey.direction_L,
+// 'Direction_R': TerminalKey.direction_R,
+  'Space': TerminalKey.space,
+  // 'Any': TerminalKey.any,
+  // 'Exclam': TerminalKey.exclam,
+  // 'QuoteDbl': TerminalKey.quoteDbl,
+  // 'NumberSign': TerminalKey.numberSign,
+  // 'Dollar': TerminalKey.dollar,
+  // 'Percent': TerminalKey.percent,
+  // 'Ampersand': TerminalKey.ampersand,
+  // 'Apostrophe': TerminalKey.apostrophe,
+  'ParenLeft': TerminalKey.numpadParenLeft,
+  'ParenRight': TerminalKey.numpadParenRight,
+  // 'Asterisk': TerminalKey.asterisk,
+  // 'Plus': TerminalKey.plus,
+  'Comma': TerminalKey.comma,
+  'Minus': TerminalKey.minus,
+  'Period': TerminalKey.period,
+  'Slash': TerminalKey.slash,
+  '0': TerminalKey.digit0,
+  '1': TerminalKey.digit1,
+  '2': TerminalKey.digit2,
+  '3': TerminalKey.digit3,
+  '4': TerminalKey.digit4,
+  '5': TerminalKey.digit5,
+  '6': TerminalKey.digit6,
+  '7': TerminalKey.digit7,
+  '8': TerminalKey.digit8,
+  '9': TerminalKey.digit9,
+// 'Colon': TerminalKey.colon,
+  'Semicolon': TerminalKey.semicolon,
+// 'Less': TerminalKey.less,
+// 'Equal': TerminalKey.equal,
+// 'Greater': TerminalKey.greater,
+// 'Question': TerminalKey.question,
+// 'At': TerminalKey.at,
+  'A': TerminalKey.keyA,
+  'B': TerminalKey.keyB,
+  'C': TerminalKey.keyC,
+  'D': TerminalKey.keyD,
+  'E': TerminalKey.keyE,
+  'F': TerminalKey.keyF,
+  'G': TerminalKey.keyG,
+  'H': TerminalKey.keyH,
+  'I': TerminalKey.keyI,
+  'J': TerminalKey.keyJ,
+  'K': TerminalKey.keyK,
+  'L': TerminalKey.keyL,
+  'M': TerminalKey.keyM,
+  'N': TerminalKey.keyN,
+  'O': TerminalKey.keyO,
+  'P': TerminalKey.keyP,
+  'Q': TerminalKey.keyQ,
+  'R': TerminalKey.keyR,
+  'S': TerminalKey.keyS,
+  'T': TerminalKey.keyT,
+  'U': TerminalKey.keyU,
+  'V': TerminalKey.keyV,
+  'W': TerminalKey.keyW,
+  'X': TerminalKey.keyX,
+  'Y': TerminalKey.keyY,
+  'Z': TerminalKey.keyZ,
+  'BracketLeft': TerminalKey.bracketLeft,
+  'Backslash': TerminalKey.backslash,
+  'BracketRight': TerminalKey.bracketRight,
+// 'AsciiCircum': TerminalKey.asciiCircum,
+  // 'Underscore': TerminalKey.underscore,
+  // 'QuoteLeft': TerminalKey.quoteLeft,
+// 'BraceLeft': TerminalKey.braceLeft,
+  // 'Bar': TerminalKey.bar,
+// 'BraceRight': TerminalKey.braceRight,
+  // 'AsciiTilde': TerminalKey.asciiTilde,
+// 'nobreakspace': TerminalKey.nobreakspace,
+// 'exclamdown': TerminalKey.exclamdown,
+// 'cent': TerminalKey.cent,
+// 'sterling': TerminalKey.sterling,
+// 'currency': TerminalKey.currency,
+// 'yen': TerminalKey.yen,
+// 'brokenbar': TerminalKey.brokenbar,
+// 'section': TerminalKey.section,
+// 'diaeresis': TerminalKey.diaeresis,
+// 'copyright': TerminalKey.copyright,
+// 'ordfeminine': TerminalKey.ordfeminine,
+// 'guillemotleft': TerminalKey.guillemotleft,
+// 'notsign': TerminalKey.notsign,
+// 'hyphen': TerminalKey.hyphen,
+// 'registered': TerminalKey.registered,
+// 'macron': TerminalKey.macron,
+// 'degree': TerminalKey.degree,
+// 'plusminus': TerminalKey.plusminus,
+// 'twosuperior': TerminalKey.twosuperior,
+// 'threesuperior': TerminalKey.threesuperior,
+// 'acute': TerminalKey.acute,
+// // 'mu': TerminalKey.mu,
+// 'paragraph': TerminalKey.paragraph,
+// 'periodcentered': TerminalKey.periodcentered,
+// 'cedilla': TerminalKey.cedilla,
+// 'onesuperior': TerminalKey.onesuperior,
+// 'masculine': TerminalKey.masculine,
+// 'guillemotright': TerminalKey.guillemotright,
+// 'onequarter': TerminalKey.onequarter,
+// 'onehalf': TerminalKey.onehalf,
+// 'threequarters': TerminalKey.threequarters,
+// 'questiondown': TerminalKey.questiondown,
+// 'Agrave': TerminalKey.agrave,
+// 'Aacute': TerminalKey.aacute,
+// 'Acircumflex': TerminalKey.acircumflex,
+// 'Atilde': TerminalKey.atilde,
+// 'Adiaeresis': TerminalKey.adiaeresis,
+// 'Aring': TerminalKey.aring,
+// 'AE': TerminalKey.aE,
+// 'Ccedilla': TerminalKey.ccedilla,
+// 'Egrave': TerminalKey.egrave,
+// 'Eacute': TerminalKey.eacute,
+// 'Ecircumflex': TerminalKey.ecircumflex,
+// 'Ediaeresis': TerminalKey.ediaeresis,
+// 'Igrave': TerminalKey.igrave,
+// 'Iacute': TerminalKey.iacute,
+// 'Icircumflex': TerminalKey.icircumflex,
+// 'Idiaeresis': TerminalKey.idiaeresis,
+// 'ETH': TerminalKey.eTH,
+// 'Ntilde': TerminalKey.ntilde,
+// 'Ograve': TerminalKey.ograve,
+// 'Oacute': TerminalKey.oacute,
+// 'Ocircumflex': TerminalKey.ocircumflex,
+// 'Otilde': TerminalKey.otilde,
+// 'Odiaeresis': TerminalKey.odiaeresis,
+// 'multiply': TerminalKey.multiply,
+// 'Ooblique': TerminalKey.ooblique,
+// 'Ugrave': TerminalKey.ugrave,
+// 'Uacute': TerminalKey.uacute,
+// 'Ucircumflex': TerminalKey.ucircumflex,
+// 'Udiaeresis': TerminalKey.udiaeresis,
+// 'Yacute': TerminalKey.yacute,
+// 'THORN': TerminalKey.tHORN,
+// 'ssharp': TerminalKey.ssharp,
+// 'division': TerminalKey.division,
+// 'ydiaeresis': TerminalKey.ydiaeresis,
+// 'Multi_key': TerminalKey.multi_key,
+// 'Codeinput': TerminalKey.codeinput,
+// 'SingleCandidate': TerminalKey.singleCandidate,
+// 'MultipleCandidate': TerminalKey.multipleCandidate,
+// 'PreviousCandidate': TerminalKey.previousCandidate,
+// 'Mode_switch': TerminalKey.mode_switch,
+// 'Kanji': TerminalKey.kanji,
+// 'Muhenkan': TerminalKey.muhenkan,
+// 'Henkan': TerminalKey.henkan,
+// 'Romaji': TerminalKey.romaji,
+// 'Hiragana': TerminalKey.hiragana,
+// 'Katakana': TerminalKey.katakana,
+// 'Hiragana_Katakana': TerminalKey.hiragana_Katakana,
+// 'Zenkaku': TerminalKey.zenkaku,
+// 'Hankaku': TerminalKey.hankaku,
+// 'Zenkaku_Hankaku': TerminalKey.zenkaku_Hankaku,
+// 'Touroku': TerminalKey.touroku,
+// 'Massyo': TerminalKey.massyo,
+// 'Kana_Lock': TerminalKey.kana_Lock,
+// 'Kana_Shift': TerminalKey.kana_Shift,
+// 'Eisu_Shift': TerminalKey.eisu_Shift,
+// 'Eisu_toggle': TerminalKey.eisu_toggle,
+// 'Hangul': TerminalKey.hangul,
+// 'Hangul_Start': TerminalKey.hangul_Start,
+// 'Hangul_End': TerminalKey.hangul_End,
+// 'Hangul_Hanja': TerminalKey.hangul_Hanja,
+// 'Hangul_Jamo': TerminalKey.hangul_Jamo,
+// 'Hangul_Romaja': TerminalKey.hangul_Romaja,
+// 'Hangul_Jeonja': TerminalKey.hangul_Jeonja,
+// 'Hangul_Banja': TerminalKey.hangul_Banja,
+// 'Hangul_PreHanja': TerminalKey.hangul_PreHanja,
+// 'Hangul_PostHanja': TerminalKey.hangul_PostHanja,
+// 'Hangul_Special': TerminalKey.hangul_Special,
+// 'Dead_Grave': TerminalKey.dead_Grave,
+// 'Dead_Acute': TerminalKey.dead_Acute,
+// 'Dead_Circumflex': TerminalKey.dead_Circumflex,
+// 'Dead_Tilde': TerminalKey.dead_Tilde,
+// 'Dead_Macron': TerminalKey.dead_Macron,
+// 'Dead_Breve': TerminalKey.dead_Breve,
+// 'Dead_Abovedot': TerminalKey.dead_Abovedot,
+// 'Dead_Diaeresis': TerminalKey.dead_Diaeresis,
+// 'Dead_Abovering': TerminalKey.dead_Abovering,
+// 'Dead_Doubleacute': TerminalKey.dead_Doubleacute,
+// 'Dead_Caron': TerminalKey.dead_Caron,
+// 'Dead_Cedilla': TerminalKey.dead_Cedilla,
+// 'Dead_Ogonek': TerminalKey.dead_Ogonek,
+// 'Dead_Iota': TerminalKey.dead_Iota,
+// 'Dead_Voiced_Sound': TerminalKey.dead_Voiced_Sound,
+// 'Dead_Semivoiced_Sound': TerminalKey.dead_Semivoiced_Sound,
+// 'Dead_Belowdot': TerminalKey.dead_Belowdot,
+// 'Dead_Hook': TerminalKey.dead_Hook,
+// 'Dead_Horn': TerminalKey.dead_Horn,
+// 'Dead_Stroke': TerminalKey.dead_Stroke,
+// 'Dead_Abovecomma': TerminalKey.dead_Abovecomma,
+// 'Dead_Abovereversedcomma': TerminalKey.dead_Abovereversedcomma,
+// 'Dead_Doublegrave': TerminalKey.dead_Doublegrave,
+// 'Dead_Belowring': TerminalKey.dead_Belowring,
+// 'Dead_Belowmacron': TerminalKey.dead_Belowmacron,
+// 'Dead_Belowcircumflex': TerminalKey.dead_Belowcircumflex,
+// 'Dead_Belowtilde': TerminalKey.dead_Belowtilde,
+// 'Dead_Belowbreve': TerminalKey.dead_Belowbreve,
+// 'Dead_Belowdiaeresis': TerminalKey.dead_Belowdiaeresis,
+// 'Dead_Invertedbreve': TerminalKey.dead_Invertedbreve,
+// 'Dead_Belowcomma': TerminalKey.dead_Belowcomma,
+// 'Dead_Currency': TerminalKey.dead_Currency,
+// 'Dead_a': TerminalKey.dead_a,
+// 'Dead_A': TerminalKey.dead_A,
+// 'Dead_e': TerminalKey.dead_e,
+// 'Dead_E': TerminalKey.dead_E,
+// 'Dead_i': TerminalKey.dead_i,
+// 'Dead_I': TerminalKey.dead_I,
+// 'Dead_o': TerminalKey.dead_o,
+// 'Dead_O': TerminalKey.dead_O,
+// 'Dead_u': TerminalKey.dead_u,
+// 'Dead_U': TerminalKey.dead_U,
+// 'Dead_Small_Schwa': TerminalKey.dead_Small_Schwa,
+// 'Dead_Capital_Schwa': TerminalKey.dead_Capital_Schwa,
+// 'Dead_Greek': TerminalKey.dead_Greek,
+// 'Dead_Lowline': TerminalKey.dead_Lowline,
+// 'Dead_Aboveverticalline': TerminalKey.dead_Aboveverticalline,
+// 'Dead_Belowverticalline': TerminalKey.dead_Belowverticalline,
+// 'Dead_Longsolidusoverlay': TerminalKey.dead_Longsolidusoverlay,
+// 'Back': TerminalKey.back,
+// 'Forward': TerminalKey.forward,
+// 'Stop': TerminalKey.stop,
+// 'Refresh': TerminalKey.refresh,
+  'VolumeDown': TerminalKey.audioVolumeDown,
+  'VolumeMute': TerminalKey.audioVolumeMute,
+  'VolumeUp': TerminalKey.audioVolumeUp,
+  'BassBoost': TerminalKey.bassBoost,
+// 'BassUp': TerminalKey.bassUp,
+// 'BassDown': TerminalKey.bassDown,
+// 'TrebleUp': TerminalKey.trebleUp,
+// 'TrebleDown': TerminalKey.trebleDown,
+  'MediaPlay': TerminalKey.mediaPlay,
+  'MediaStop': TerminalKey.mediaStop,
+// 'MediaPrevious': TerminalKey.mediaPrevious,
+// 'MediaNext': TerminalKey.mediaNext,
+  'MediaRecord': TerminalKey.mediaRecord,
+  'MediaPause': TerminalKey.mediaPause,
+  'MediaTogglePlayPause': TerminalKey.mediaPlayPause,
+  'HomePage': TerminalKey.browserHome,
+// 'Favorites': TerminalKey.favorites,
+// 'Search': TerminalKey.search,
+// 'Standby': TerminalKey.standby,
+// 'OpenUrl': TerminalKey.openUrl,
+// 'LaunchMail': TerminalKey.launchMail,
+// 'LaunchMedia': TerminalKey.launchMedia,
+// 'Launch0': TerminalKey.launch0,
+// 'Launch1': TerminalKey.launch1,
+// 'Launch2': TerminalKey.launch2,
+// 'Launch3': TerminalKey.launch3,
+// 'Launch4': TerminalKey.launch4,
+// 'Launch5': TerminalKey.launch5,
+// 'Launch6': TerminalKey.launch6,
+// 'Launch7': TerminalKey.launch7,
+// 'Launch8': TerminalKey.launch8,
+// 'Launch9': TerminalKey.launch9,
+// 'LaunchA': TerminalKey.launchA,
+// 'LaunchB': TerminalKey.launchB,
+// 'LaunchC': TerminalKey.launchC,
+// 'LaunchD': TerminalKey.launchD,
+// 'LaunchE': TerminalKey.launchE,
+// 'LaunchF': TerminalKey.launchF,
+// 'LaunchG': TerminalKey.launchG,
+// 'LaunchH': TerminalKey.launchH,
+  'MonBrightnessUp': TerminalKey.brightnessUp,
+  'MonBrightnessDown': TerminalKey.brightnessDown,
+// 'KeyboardLightOnOff': TerminalKey.keyboardLightOnOff,
+// 'KeyboardBrightnessUp': TerminalKey.keyboardBrightnessUp,
+// 'KeyboardBrightnessDown': TerminalKey.keyboardBrightnessDown,
+  'PowerOff': TerminalKey.power,
+  'WakeUp': TerminalKey.wakeUp,
+  'Eject': TerminalKey.eject,
+// 'ScreenSaver': TerminalKey.screenSaver,
+// 'WWW': TerminalKey.wWW,
+// 'Memo': TerminalKey.memo,
+// 'LightBulb': TerminalKey.lightBulb,
+// 'Shop': TerminalKey.shop,
+// 'History': TerminalKey.history,
+// 'AddFavorite': TerminalKey.addFavorite,
+// 'HotLinks': TerminalKey.hotLinks,
+// 'BrightnessAdjust': TerminalKey.brightnessAdjust,
+// 'Finance': TerminalKey.finance,
+// 'Community': TerminalKey.community,
+// 'AudioRewind': TerminalKey.audioRewind,
+// 'BackForward': TerminalKey.backForward,
+// 'ApplicationLeft': TerminalKey.applicationLeft,
+// 'ApplicationRight': TerminalKey.applicationRight,
+// 'Book': TerminalKey.book,
+// 'CD': TerminalKey.cD,
+// 'Calculator': TerminalKey.calculator,
+// 'ToDoList': TerminalKey.toDoList,
+// 'ClearGrab': TerminalKey.clearGrab,
+  'Close': TerminalKey.close,
+  'Copy': TerminalKey.copy,
+  'Cut': TerminalKey.cut,
+// 'Display': TerminalKey.display,
+// 'DOS': TerminalKey.dOS,
+// 'Documents': TerminalKey.documents,
+// 'Excel': TerminalKey.excel,
+// 'Explorer': TerminalKey.explorer,
+// 'Game': TerminalKey.game,
+// 'Go': TerminalKey.go,
+// 'iTouch': TerminalKey.iTouch,
+// 'LogOff': TerminalKey.logOff,
+// 'Market': TerminalKey.market,
+// 'Meeting': TerminalKey.meeting,
+// 'MenuKB': TerminalKey.menuKB,
+// 'MenuPB': TerminalKey.menuPB,
+// 'MySites': TerminalKey.mySites,
+// 'News': TerminalKey.news,
+// 'OfficeHome': TerminalKey.officeHome,
+// 'Option': TerminalKey.option,
+// 'Paste': TerminalKey.paste,
+// 'Phone': TerminalKey.phone,
+// 'Calendar': TerminalKey.calendar,
+// 'Reply': TerminalKey.reply,
+// 'Reload': TerminalKey.reload,
+// 'RotateWindows': TerminalKey.rotateWindows,
+// 'RotationPB': TerminalKey.rotationPB,
+// 'RotationKB': TerminalKey.rotationKB,
+  'Save': TerminalKey.save,
+// 'Send': TerminalKey.send,
+// 'Spell': TerminalKey.spell,
+// 'SplitScreen': TerminalKey.splitScreen,
+// 'Support': TerminalKey.support,
+// 'TaskPane': TerminalKey.taskPane,
+// 'Terminal': TerminalKey.terminal,
+// 'Tools': TerminalKey.tools,
+// 'Travel': TerminalKey.travel,
+// 'Video': TerminalKey.video,
+// 'Word': TerminalKey.word,
+// 'Xfer': TerminalKey.xfer,
+  'ZoomIn': TerminalKey.zoomIn,
+  'ZoomOut': TerminalKey.zoomOut,
+// 'Away': TerminalKey.away,
+// 'Messenger': TerminalKey.messenger,
+// 'WebCam': TerminalKey.webCam,
+// 'MailForward': TerminalKey.mailForward,
+// 'Pictures': TerminalKey.pictures,
+// 'Music': TerminalKey.music,
+// 'Battery': TerminalKey.battery,
+// 'Bluetooth': TerminalKey.bluetooth,
+// 'WLAN': TerminalKey.wLAN,
+// 'UWB': TerminalKey.uWB,
+// 'AudioForward': TerminalKey.audioForward,
+// 'AudioRepeat': TerminalKey.audioRepeat,
+// 'AudioRandomPlay': TerminalKey.audioRandomPlay,
+// 'Subtitle': TerminalKey.subtitle,
+// 'AudioCycleTrack': TerminalKey.audioCycleTrack,
+// 'Time': TerminalKey.time,
+// 'Hibernate': TerminalKey.hibernate,
+// 'View': TerminalKey.view,
+// 'TopMenu': TerminalKey.topMenu,
+// 'PowerDown': TerminalKey.powerDown,
+// 'Suspend': TerminalKey.suspend,
+// 'ContrastAdjust': TerminalKey.contrastAdjust,
+// 'TouchpadToggle': TerminalKey.touchpadToggle,
+// 'TouchpadOn': TerminalKey.touchpadOn,
+// 'TouchpadOff': TerminalKey.touchpadOff,
+// 'MicMute': TerminalKey.micMute,
+// 'Red': TerminalKey.red,
+// 'Green': TerminalKey.green,
+// 'Yellow': TerminalKey.yellow,
+// 'Blue': TerminalKey.blue,
+  'ChannelUp': TerminalKey.channelUp,
+  'ChannelDown': TerminalKey.channelDown,
+// 'Guide': TerminalKey.guide,
+  'Info': TerminalKey.info,
+// 'Settings': TerminalKey.settings,
+// 'MicVolumeUp': TerminalKey.micVolumeUp,
+// 'MicVolumeDown': TerminalKey.micVolumeDown,
+// 'New': TerminalKey.new,
+  'Open': TerminalKey.open,
+  'Find': TerminalKey.find,
+  'Undo': TerminalKey.undo,
+  'Redo': TerminalKey.redo,
+  'MediaLast': TerminalKey.mediaLast,
+// 'unknown': TerminalKey.unknown,
+// 'Call': TerminalKey.call,
+// 'Camera': TerminalKey.camera,
+// 'CameraFocus': TerminalKey.cameraFocus,
+// 'Context1': TerminalKey.context1,
+// 'Context2': TerminalKey.context2,
+// 'Context3': TerminalKey.context3,
+// 'Context4': TerminalKey.context4,
+// 'Flip': TerminalKey.flip,
+// 'Hangup': TerminalKey.hangup,
+// 'No': TerminalKey.no,
+  'Select': TerminalKey.select,
+// 'Yes': TerminalKey.yes,
+// 'ToggleCallHangup': TerminalKey.toggleCallHangup,
+// 'VoiceDial': TerminalKey.voiceDial,
+// 'LastNumberRedial': TerminalKey.lastNumberRedial,
+// 'Execute': TerminalKey.execute,
+// 'Printer': TerminalKey.printer,
+// 'Play': TerminalKey.play,
+  'Sleep': TerminalKey.sleep,
+// 'Zoom': TerminalKey.zoom,
+  'Exit': TerminalKey.exit,
+// 'Cancel': TerminalKey.cancel,
+};

+ 1 - 0
lib/input/shortcut.dart

@@ -0,0 +1 @@
+// TBD

+ 16 - 0
lib/mouse/mouse_kind.dart

@@ -0,0 +1,16 @@
+enum MouseKind {
+  /// A touch-based pointer device.
+  touch,
+
+  /// A mouse-based pointer device.
+  mouse,
+
+  /// A pointer device with a stylus.
+  stylus,
+
+  /// A pointer device with a stylus that has been inverted.
+  invertedStylus,
+
+  /// An unknown pointer device.
+  unknown
+}

+ 58 - 0
lib/mouse/mouse_mode.dart

@@ -0,0 +1,58 @@
+import 'package:xterm/mouse/position.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+abstract class MouseMode {
+  const MouseMode();
+
+  static const none = MouseModeNone();
+  // static const x10 = MouseModeX10();
+  // static const vt200 = MouseModeX10();
+  // static const buttonEvent = MouseModeX10();
+
+  void onTap(Terminal terminal, Position offset);
+  void onPanStart(Terminal terminal, Position offset) {}
+  void onPanUpdate(Terminal terminal, Position offset) {}
+}
+
+class MouseModeNone extends MouseMode {
+  const MouseModeNone();
+
+  @override
+  void onTap(Terminal terminal, Position offset) {
+    terminal.debug.onMsg('tap: $offset');
+    terminal.selection.clear();
+  }
+
+  @override
+  void onPanStart(Terminal terminal, Position offset) {
+    terminal.selection.init(offset);
+  }
+
+  @override
+  void onPanUpdate(Terminal terminal, Position offset) {
+    terminal.selection.update(offset);
+  }
+}
+
+class MouseModeX10 extends MouseMode {
+  const MouseModeX10();
+
+  @override
+  void onTap(Terminal terminal, Position offset) {
+    final btn = 1;
+
+    final px = offset.x + 1;
+    final py = terminal.buffer.convertRawLineToViewLine(offset.y) + 1;
+
+    final buffer = StringBuffer();
+    buffer.writeCharCode(0x1b);
+    buffer.write('[M');
+    buffer.writeCharCode(btn + 32);
+    buffer.writeCharCode(px + 32);
+    buffer.writeCharCode(py + 32);
+
+    if (terminal.onInput != null) {
+      terminal.onInput(buffer.toString());
+    }
+  }
+}

+ 43 - 0
lib/mouse/position.dart

@@ -0,0 +1,43 @@
+class Position {
+  const Position(this.x, this.y);
+
+  final int x;
+  final int y;
+
+  bool isBefore(Position another) {
+    if (another == null) {
+      return false;
+    }
+
+    return another.y > y || (another.y == y && another.x > x);
+  }
+
+  bool isAfter(Position another) {
+    if (another == null) {
+      return false;
+    }
+
+    return another.y < y || (another.y == y && another.x < x);
+  }
+
+  bool isBeforeOrSame(Position another) {
+    if (another == null) {
+      return false;
+    }
+
+    return another.y > y || (another.y == y && another.x >= x);
+  }
+
+  bool isAfterOrSame(Position another) {
+    if (another == null) {
+      return false;
+    }
+
+    return another.y < y || (another.y == y && another.x <= x);
+  }
+
+  @override
+  String toString() {
+    return 'MouseOffset($x, $y)';
+  }
+}

+ 58 - 0
lib/mouse/selection.dart

@@ -0,0 +1,58 @@
+import 'package:xterm/mouse/position.dart';
+
+class Selection {
+  Position _start;
+  Position _end;
+  var _endFixed = false;
+
+  Position get start => _start;
+  Position get end => _end;
+
+  void init(Position position) {
+    _start = position;
+    _end = position;
+    _endFixed = false;
+  }
+
+  void update(Position position) {
+    if (_start == null) {
+      return;
+    }
+
+    if (position.isBefore(_start) && !_endFixed) {
+      _endFixed = true;
+      _end = _start;
+    }
+
+    if (position.isAfter(_start) && _endFixed) {
+      _endFixed = false;
+      _start = _end;
+    }
+
+    if (_endFixed) {
+      _start = position;
+    } else {
+      _end = position;
+    }
+
+    // print('($_start, $end');
+  }
+
+  void clear() {
+    _start = null;
+    _end = null;
+    _endFixed = false;
+  }
+
+  bool contains(Position position) {
+    if (isEmpty) {
+      return false;
+    }
+
+    return _start.isBeforeOrSame(position) && _end.isAfterOrSame(position);
+  }
+
+  bool get isEmpty {
+    return _start == null || _end == null;
+  }
+}

+ 84 - 0
lib/terminal/ansi.dart

@@ -0,0 +1,84 @@
+import 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:xterm/terminal/csi.dart';
+import 'package:xterm/terminal/osc.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+typedef AnsiHandler = FutureOr<void> Function(StreamQueue<int>, Terminal);
+
+Future<void> ansiHandler(StreamQueue<int> queue, Terminal terminal) async {
+  final charAfterEsc = String.fromCharCode(await queue.next);
+
+  final handler = _ansiHandlers[charAfterEsc];
+  if (handler != null) {
+    if (handler != csiHandler && handler != oscHandler) {
+      terminal.debug.onEsc(charAfterEsc);
+    }
+    return handler(queue, terminal);
+  }
+
+  terminal.debug.onError('unsupported ansi sequence: $charAfterEsc');
+}
+
+final _ansiHandlers = <String, AnsiHandler>{
+  '[': csiHandler,
+  ']': oscHandler,
+  '7': _ansiSaveCursorHandler,
+  '8': _ansiRestoreCursorHandler,
+  'D': _ansiIndexHandler,
+  'E': _ansiNextLineHandler,
+  'H': _ansiTabSetHandler,
+  'M': _ansiReverseIndexHandler,
+  'P': _unsupportedHandler, // Sixel
+  'c': _unsupportedHandler,
+  '#': _unsupportedHandler,
+  '(': _scsHandler(0), //  G0
+  ')': _scsHandler(1), //  G1
+  '*': _voidHandler(1), // TODO: G2 (vt220)
+  '+': _voidHandler(1), // TODO: G3 (vt220)
+  '>': _voidHandler(0), // TODO: Normal Keypad
+  '=': _voidHandler(0), // TODO: Application Keypad
+};
+
+AnsiHandler _voidHandler(int sequenceLength) {
+  return (queue, terminal) {
+    return queue.take(sequenceLength);
+  };
+}
+
+void _unsupportedHandler(StreamQueue<int> queue, Terminal terminal) async {
+  // print('unimplemented ansi sequence.');
+}
+
+void _ansiSaveCursorHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.buffer.saveCursor();
+}
+
+void _ansiRestoreCursorHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.buffer.restoreCursor();
+}
+
+void _ansiIndexHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.buffer.index();
+}
+
+void _ansiReverseIndexHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.buffer.reverseIndex();
+}
+
+AnsiHandler _scsHandler(int which) {
+  return (StreamQueue<int> queue, Terminal terminal) async {
+    final name = String.fromCharCode(await queue.next);
+    terminal.buffer.charset.designate(which, name);
+  };
+}
+
+void _ansiNextLineHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.buffer.newLine();
+  terminal.buffer.setCursorX(0);
+}
+
+void _ansiTabSetHandler(StreamQueue<int> queue, Terminal terminal) {
+  terminal.tabSetAtCursor();
+}

+ 95 - 0
lib/terminal/charset.dart

@@ -0,0 +1,95 @@
+typedef CharsetTranslator = int Function(int);
+
+const _charsets = <String, CharsetTranslator>{
+  '0': decSpecGraphicsTranslator,
+  'B': asciiTranslator,
+};
+
+class Charset {
+  var _charsetMap = <int, CharsetTranslator>{};
+  var _currentIndex = 0;
+
+  var _savedCharsetMap = <int, CharsetTranslator>{};
+  var _savedIndex = 0;
+
+  var _cached = asciiTranslator;
+
+  void _updateCache() {
+    _cached = _charsetMap[_currentIndex] ?? asciiTranslator;
+  }
+
+  int translate(int codePoint) {
+    return _cached(codePoint);
+  }
+
+  void designate(int index, String name) {
+    final charset = _charsets[name];
+    if (charset != null) {
+      _charsetMap[index] = charset;
+      _updateCache();
+    }
+  }
+
+  void use(int index) {
+    _currentIndex = index;
+    _updateCache();
+  }
+
+  void save() {
+    _savedCharsetMap = Map.from(_charsetMap);
+    _savedIndex = _currentIndex;
+  }
+
+  void restore() {
+    _charsetMap = _savedCharsetMap ?? _charsetMap;
+    _currentIndex = _savedIndex ?? _currentIndex;
+    _updateCache();
+  }
+}
+
+const decSpecGraphics = <int, int>{
+  0x5f: 0x00A0, // NO-BREAK SPACE
+  0x60: 0x25C6, // BLACK DIAMOND
+  0x61: 0x2592, // MEDIUM SHADE
+  0x62: 0x2409, // SYMBOL FOR HORIZONTAL TABULATION
+  0x63: 0x240C, // SYMBOL FOR FORM FEED
+  0x64: 0x240D, // SYMBOL FOR CARRIAGE RETURN
+  0x65: 0x240A, // SYMBOL FOR LINE FEED
+  0x66: 0x00B0, // DEGREE SIGN
+  0x67: 0x00B1, // PLUS-MINUS SIGN
+  0x68: 0x2424, // SYMBOL FOR NEWLINE
+  0x69: 0x240B, // SYMBOL FOR VERTICAL TABULATION
+  0x6a: 0x2518, // BOX DRAWINGS LIGHT UP AND LEFT
+  0x6b: 0x2510, // BOX DRAWINGS LIGHT DOWN AND LEFT
+  0x6c: 0x250C, // BOX DRAWINGS LIGHT DOWN AND RIGHT
+  0x6d: 0x2514, // BOX DRAWINGS LIGHT UP AND RIGHT
+  0x6e: 0x253C, // BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL
+  0x6f: 0x23BA, // HORIZONTAL SCAN LINE-1
+  0x70: 0x23BB, // HORIZONTAL SCAN LINE-3
+  0x71: 0x2500, // BOX DRAWINGS LIGHT HORIZONTAL
+  0x72: 0x23BC, // HORIZONTAL SCAN LINE-7
+  0x73: 0x23BD, // HORIZONTAL SCAN LINE-9
+  0x74: 0x251C, // BOX DRAWINGS LIGHT VERTICAL AND RIGHT
+  0x75: 0x2524, // BOX DRAWINGS LIGHT VERTICAL AND LEFT
+  0x76: 0x2534, // BOX DRAWINGS LIGHT UP AND HORIZONTAL
+  0x77: 0x252C, // BOX DRAWINGS LIGHT DOWN AND HORIZONTAL
+  0x78: 0x2502, // BOX DRAWINGS LIGHT VERTICAL
+  0x79: 0x2264, // LESS-THAN OR EQUAL TO
+  0x7a: 0x2265, // GREATER-THAN OR EQUAL TO
+  0x7b: 0x03C0, // GREEK SMALL LETTER PI
+  0x7c: 0x2260, // NOT EQUAL TO
+  0x7d: 0x00A3, // POUND SIGN
+  0x7e: 0x00B7, // MIDDLE DOT
+};
+
+int asciiTranslator(int codePoint) {
+  return codePoint;
+}
+
+int decSpecGraphicsTranslator(int codePoint) {
+  if (codePoint >= 127) {
+    return codePoint;
+  }
+
+  return decSpecGraphics[codePoint] ?? decSpecGraphics;
+}

+ 401 - 0
lib/terminal/csi.dart

@@ -0,0 +1,401 @@
+import 'package:async/async.dart';
+import 'package:xterm/terminal/modes.dart';
+import 'package:xterm/terminal/sgr.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+typedef CsiSequenceHandler = void Function(CSI, Terminal);
+
+final _csiHandlers = <String, CsiSequenceHandler>{
+  'c': csiSendDeviceAttributesHandler,
+  'd': csiLinePositionAbsolute,
+  'f': csiCursorPositionHandler,
+  'g': csiTabClearHandler,
+  'h': csiModeHandler,
+  'l': csiModeHandler,
+  'm': sgrHandler,
+  'n': csiDeviceStatusReportHandler,
+  'r': csiSetMarginsHandler,
+  't': csiWindowManipulation,
+  'A': csiCursorUpHandler,
+  'B': csiCursorDownHandler,
+  'C': csiCursorForwardHandler,
+  'D': csiCursorBackwardHandler,
+  'E': csiCursorNextLineHandler,
+  'F': csiCursorPrecedingLineHandler,
+  'G': csiCursorHorizontalAbsoluteHandler,
+  'H': csiCursorPositionHandler,
+  'J': csiEraseInDisplayHandler,
+  'K': csiEraseInLineHandler,
+  'L': csiInsertLinesHandler,
+  'M': csiDeleteLinesHandler,
+  'P': csiDeleteHandler,
+  'S': csiScrollUpHandler,
+  'T': csiScrollDownHandler,
+  'X': csiEraseCharactersHandler,
+  '@': csiInsertBlankCharactersHandler,
+};
+
+class CSI {
+  CSI({
+    this.params,
+    this.finalByte,
+    this.intermediates,
+  });
+
+  final List<String> params;
+  final int finalByte;
+  final List<int> intermediates;
+
+  @override
+  String toString() {
+    return params.join(';') + String.fromCharCode(finalByte);
+  }
+}
+
+Future<CSI> _parseCsi(StreamQueue<int> queue) async {
+  final paramBuffer = StringBuffer();
+  final intermediates = <int>[];
+
+  while (true) {
+    final char = await queue.next;
+
+    if (char >= 0x30 && char <= 0x3F) {
+      paramBuffer.writeCharCode(char);
+      continue;
+    }
+
+    if (char > 0 && char <= 0x2F) {
+      intermediates.add(char);
+      continue;
+    }
+
+    const csiMin = 0x40;
+    const csiMax = 0x7e;
+
+    if (char >= csiMin && char <= csiMax) {
+      final params = paramBuffer.toString().split(';');
+      return CSI(
+        params: params,
+        finalByte: char,
+        intermediates: intermediates,
+      );
+    }
+  }
+}
+
+Future<void> csiHandler(StreamQueue<int> queue, Terminal terminal) async {
+  final csi = await _parseCsi(queue);
+
+  terminal.debug.onCsi(csi);
+
+  final handler = _csiHandlers[String.fromCharCode(csi.finalByte)];
+
+  if (handler != null) {
+    handler(csi, terminal);
+    return;
+  }
+
+  terminal.debug.onError('unknown: $csi');
+}
+
+void csiEraseInDisplayHandler(CSI csi, Terminal terminal) {
+  var ps = '0';
+
+  if (csi.params.isNotEmpty) {
+    ps = csi.params.first;
+  }
+
+  switch (ps) {
+    case '':
+    case '0':
+      terminal.buffer.eraseDisplayFromCursor();
+      break;
+    case '1':
+      terminal.buffer.eraseDisplayToCursor();
+      break;
+    case '2':
+    case '3':
+      terminal.buffer.eraseDisplay();
+      break;
+    default:
+      terminal.debug.onError("Unsupported ED: CSI $ps J");
+  }
+}
+
+void csiEraseInLineHandler(CSI csi, Terminal terminal) {
+  var ps = '0';
+
+  if (csi.params.isNotEmpty) {
+    ps = csi.params.first;
+  }
+
+  switch (ps) {
+    case '':
+    case '0':
+      terminal.buffer.eraseLineFromCursor();
+      break;
+    case '1':
+      terminal.buffer.eraseLineToCursor();
+      break;
+    case '2':
+      terminal.buffer.eraseLine();
+      break;
+    default:
+      terminal.debug.onError("Unsupported EL: CSI $ps K");
+  }
+}
+
+void csiCursorPositionHandler(CSI csi, Terminal terminal) {
+  var x = 1;
+  var y = 1;
+
+  if (csi.params.length == 2) {
+    y = int.tryParse(csi.params[0]) ?? x;
+    x = int.tryParse(csi.params[1]) ?? y;
+  }
+
+  terminal.buffer.setPosition(x - 1, y - 1);
+}
+
+void csiLinePositionAbsolute(CSI csi, Terminal terminal) {
+  var row = 1;
+
+  if (csi.params.isNotEmpty) {
+    row = int.tryParse(csi.params.first) ?? row;
+  }
+
+  terminal.buffer.setCursorY(row - 1);
+}
+
+void csiCursorHorizontalAbsoluteHandler(CSI csi, Terminal terminal) {
+  var x = 1;
+
+  if (csi.params.isNotEmpty) {
+    x = int.tryParse(csi.params.first) ?? x;
+  }
+
+  terminal.buffer.setCursorX(x - 1);
+}
+
+void csiCursorForwardHandler(CSI csi, Terminal terminal) {
+  var offset = 1;
+
+  if (csi.params.isNotEmpty) {
+    offset = int.tryParse(csi.params.first) ?? offset;
+  }
+
+  terminal.buffer.movePosition(offset, 0);
+}
+
+void csiCursorBackwardHandler(CSI csi, Terminal terminal) {
+  var offset = 1;
+
+  if (csi.params.isNotEmpty) {
+    offset = int.tryParse(csi.params.first) ?? offset;
+  }
+
+  terminal.buffer.movePosition(-offset, 0);
+}
+
+void csiEraseCharactersHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  terminal.buffer.eraseCharacters(count);
+}
+
+void csiModeHandler(CSI csi, Terminal terminal) {
+  // terminal.ActiveBuffer().ClearSelection()
+  return csiSetModes(csi, terminal);
+}
+
+void csiDeviceStatusReportHandler(CSI csi, Terminal terminal) {
+  if (csi.params.isEmpty) return;
+
+  switch (csi.params[0]) {
+    case "5":
+      terminal.onInput("\x1b[0n");
+      break;
+    case "6": // report cursor position
+      terminal.onInput("\x1b[${terminal.cursorX + 1};${terminal.cursorY + 1}R");
+      break;
+    default:
+      terminal.debug.onError('Unknown Device Status Report identifier: ${csi.params[0]}');
+      return;
+  }
+}
+
+void csiSendDeviceAttributesHandler(CSI csi, Terminal terminal) {
+  var response = '?1;2';
+
+  if (csi.params.isNotEmpty && csi.params.first.startsWith('>')) {
+    response = '>0;0;0';
+  }
+
+  terminal.onInput('\x1b[${response}c');
+}
+
+void csiCursorUpHandler(CSI csi, Terminal terminal) {
+  var distance = 1;
+
+  if (csi.params.isNotEmpty) {
+    distance = int.tryParse(csi.params.first) ?? distance;
+  }
+
+  terminal.buffer.movePosition(0, -distance);
+}
+
+void csiCursorDownHandler(CSI csi, Terminal terminal) {
+  var distance = 1;
+
+  if (csi.params.isNotEmpty) {
+    distance = int.tryParse(csi.params.first) ?? distance;
+  }
+
+  terminal.buffer.movePosition(0, distance);
+}
+
+void csiSetMarginsHandler(CSI csi, Terminal terminal) {
+  var top = 1;
+  var bottom = terminal.viewHeight;
+
+  if (csi.params.length > 2) {
+    return;
+  }
+
+  if (csi.params.isNotEmpty) {
+    top = int.tryParse(csi.params[0]) ?? top;
+
+    if (csi.params.length > 1) {
+      bottom = int.tryParse(csi.params[1]) ?? bottom;
+    }
+  }
+
+  terminal.buffer.setVerticalMargins(top - 1, bottom - 1);
+  terminal.buffer.setPosition(0, 0);
+}
+
+void csiDeleteHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.deleteChars(count);
+}
+
+void csiTabClearHandler(CSI csi, Terminal terminal) {
+  // TODO
+}
+
+void csiWindowManipulation(CSI csi, Terminal terminal) {
+  // not supported
+}
+
+void csiCursorNextLineHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.moveCursorY(count);
+  terminal.buffer.setCursorX(0);
+}
+
+void csiCursorPrecedingLineHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.moveCursorY(-count);
+  terminal.buffer.setCursorX(0);
+}
+
+void csiInsertLinesHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.insertLines(count);
+}
+
+void csiDeleteLinesHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.deleteLines(count);
+}
+
+void csiScrollUpHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.areaScrollUp(count);
+}
+
+void csiScrollDownHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.areaScrollDown(count);
+}
+
+void csiInsertBlankCharactersHandler(CSI csi, Terminal terminal) {
+  var count = 1;
+
+  if (csi.params.isNotEmpty) {
+    count = int.tryParse(csi.params.first) ?? count;
+  }
+
+  if (count < 1) {
+    count = 1;
+  }
+
+  terminal.buffer.insertBlankCharacters(count);
+}

+ 170 - 0
lib/terminal/modes.dart

@@ -0,0 +1,170 @@
+import 'package:xterm/mouse/mouse_mode.dart';
+import 'package:xterm/terminal/csi.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+final _decset = 'h'.codeUnitAt(0);
+final _decrst = 'l'.codeUnitAt(0);
+
+bool _isEnabled(int finalByte) {
+  if (finalByte == _decset) {
+    return true;
+  }
+
+  if (finalByte == _decrst) {
+    return false;
+  }
+
+  // print('unexpected finalByte: $finalByte');
+  return true;
+}
+
+void csiSetModes(CSI csi, Terminal terminal) {
+  if (csi.params.isEmpty) {
+    // print('warning: no mode specified.');
+    return;
+  }
+
+  final enabled = _isEnabled(csi.finalByte);
+
+  if (csi.params.length == 1) {
+    return csiSetMode(csi.params.first, enabled, terminal);
+  }
+
+  const decPrefix = '?';
+  final isDec = csi.params.first.startsWith(decPrefix);
+
+  for (var mode in csi.params) {
+    if (isDec && !mode.startsWith(decPrefix)) {
+      mode = decPrefix + mode;
+    }
+    csiSetMode(csi.params.first, enabled, terminal);
+  }
+}
+
+void csiSetMode(String mode, bool enabled, Terminal terminal) {
+  switch (mode) {
+    case "4":
+      if (enabled) {
+        terminal.setInsertMode();
+      } else {
+        terminal.setReplaceMode();
+      }
+      break;
+    case "20":
+      if (enabled) {
+        terminal.setNewLineMode();
+      } else {
+        terminal.setLineFeedMode();
+      }
+      break;
+    case "?1":
+      terminal.setApplicationCursorKeys(enabled);
+      break;
+    // case "?3":
+    // 	if (enabled) {
+    // 		// DECCOLM - COLumn mode, 132 characters per line
+    // 		terminal.setSize(132, uint(lines));
+    // 	} else {
+    // 		// DECCOLM - 80 characters per line (erases screen)
+    // 		terminal.setSize(80, uint(lines));
+    // 	}
+    // 	terminal.clear();
+    // case "?4":
+    // 	// DECSCLM
+    case "?5":
+      // DECSCNM
+      terminal.setScreenMode(enabled);
+      break;
+    case "?6":
+      // DECOM
+      terminal.setOriginMode(enabled);
+      break;
+    case "?7":
+      //DECAWM
+      terminal.setAutoWrapMode(enabled);
+      break;
+    case "?9":
+    	if (enabled) {
+    		// terminal.setMouseMode(MouseMode.x10);
+    	} else {
+    		terminal.setMouseMode(MouseMode.none);
+    	}
+     break;
+    case "?12":
+    case "?13":
+      terminal.setBlinkingCursor(enabled);
+      break;
+    case "?25":
+      terminal.setShowCursor(enabled);
+      break;
+    case "?47":
+    case "?1047":
+      if (enabled) {
+        terminal.useAltBuffer();
+      } else {
+        terminal.useMainBuffer();
+      }
+      break;
+    case "?1000":
+    case "?10061000":
+      // enable mouse tracking
+      // 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
+      if (enabled) {
+        // terminal.setMouseMode(MouseMode.vt200);
+      } else {
+        terminal.setMouseMode(MouseMode.none);
+      }
+      break;
+    case "?1002":
+      // enable mouse tracking
+      // 1000 refers to ext mode for extended mouse click area - otherwise only x <= 255-31
+      if (enabled) {
+        // terminal.setMouseMode(MouseMode.buttonEvent);
+      } else {
+        terminal.setMouseMode(MouseMode.none);
+      }
+      break;
+    case "?1003":
+      if (enabled) {
+        // terminal.setMouseMode(MouseMode.anyEvent);
+      } else {
+        terminal.setMouseMode(MouseMode.none);
+      }
+      break;
+    case "?1005":
+      if (enabled) {
+        // terminal.setMouseExtMode(MouseExt.utf);
+      } else {
+        // terminal.setMouseExtMode(MouseExt.none);
+      }
+      break;
+    case "?1006":
+      if (enabled) {
+        // terminal.setMouseExtMode(MouseExt.sgr);
+      } else {
+        // terminal.setMouseExtMode(MouseExt.none);
+      }
+      break;
+    case "?1048":
+      if (enabled) {
+        terminal.buffer.saveCursor();
+      } else {
+        terminal.buffer.restoreCursor();
+      }
+      break;
+    case "?1049":
+      if (enabled) {
+        terminal.useAltBuffer();
+        terminal.buffer.clear();
+      } else {
+        terminal.useMainBuffer();
+      }
+      break;
+    case "?2004":
+      terminal.setBracketedPasteMode(enabled);
+      break;
+    default:
+      terminal.debug.onError('unsupported mode: $mode');
+      return;
+  }
+}

+ 65 - 0
lib/terminal/osc.dart

@@ -0,0 +1,65 @@
+import 'package:async/async.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+bool _isOscTerminator(int codePoint) {
+  const terminator = {0x07, 0x5c, 0x00};
+  return terminator.contains(codePoint);
+}
+
+Future<List<String>> _parseOsc(StreamQueue<int> queue) async {
+  final params = <String>[];
+  final param = StringBuffer();
+
+  while (true) {
+    final char = await queue.next;
+
+    if (_isOscTerminator(char)) {
+      params.add(param.toString());
+      break;
+    }
+
+    const semicolon = 59;
+    if (char == semicolon) {
+      params.add(param.toString());
+      param.clear();
+      continue;
+    }
+
+    param.writeCharCode(char);
+  }
+
+  return params;
+}
+
+Future<void> oscHandler(StreamQueue<int> queue, Terminal terminal) async {
+  final params = await _parseOsc(queue);
+  terminal.debug.onOsc(params);
+
+  if (params.isEmpty) {
+    terminal.debug.onError('osc with no params');
+    return;
+  }
+
+  if (params.length < 2) {
+    return;
+  }
+
+  final ps = params[0];
+  final pt = params[1];
+
+  switch (ps) {
+    case '0':
+    case '2':
+      if (terminal.onTitleChange != null) {
+        terminal.onTitleChange(pt);
+      }
+      break;
+    case '1':
+      if (terminal.onIconChange != null) {
+        terminal.onIconChange(pt);
+      }
+      break;
+    default:
+      terminal.debug.onError('unknown osc ps: $ps');
+  }
+}

+ 50 - 0
lib/terminal/sbc.dart

@@ -0,0 +1,50 @@
+import 'package:xterm/terminal/terminal.dart';
+
+typedef SbcHandler = void Function(int, Terminal);
+
+final sbcHandlers = <int, SbcHandler>{
+  0x05: _voidHandler,
+  0x07: _bellHandler,
+  0x08: _backspaceReturnHandler,
+  0x09: _tabHandler,
+  0x0a: _newLineHandler,
+  0x0b: _newLineHandler,
+  0x0c: _newLineHandler,
+  0x0d: _carriageReturnHandler,
+  0x0e: _shiftOutHandler,
+  0x0f: _shiftInHandler,
+};
+
+void _bellHandler(int code, Terminal terminal) {
+  if (terminal.onBell != null) {
+    terminal.onBell();
+  }
+}
+
+void _voidHandler(int code, Terminal terminal) {
+  // unsupported.
+}
+
+void _newLineHandler(int code, Terminal terminal) {
+  terminal.buffer.newLine();
+}
+
+void _carriageReturnHandler(int code, Terminal terminal) {
+  terminal.buffer.carriageReturn();
+}
+
+void _backspaceReturnHandler(int code, Terminal terminal) {
+  terminal.buffer.backspace();
+}
+
+void _shiftOutHandler(int code, Terminal terminal) {
+  terminal.buffer.charset.use(1);
+}
+
+void _shiftInHandler(int code, Terminal terminal) {
+  terminal.buffer.charset.use(0);
+}
+
+void _tabHandler(int code, Terminal terminal) {
+  terminal.tab();
+}

+ 342 - 0
lib/terminal/sgr.dart

@@ -0,0 +1,342 @@
+import 'package:xterm/buffer/cell_color.dart';
+import 'package:xterm/terminal/csi.dart';
+import 'package:xterm/terminal/terminal.dart';
+
+/// SGR selects one or more character attributes at the same time.
+/// Multiple params (up to 32) are applied from in order from left to right.
+/// The changed attributes are applied to all new characters received.
+/// If you move characters in the viewport by scrolling or any other means,
+/// then the attributes move with the characters.
+void sgrHandler(CSI csi, Terminal terminal) {
+  final params = csi.params.toList();
+
+  if (params.isEmpty) {
+    params.add('0');
+  }
+
+  for (var i = 0; i < params.length; i++) {
+    final param = params[i];
+    switch (param) {
+      case '':
+      case '0':
+      case '00':
+        terminal.cellAttr.reset(fgColor: terminal.colorScheme.foreground);
+        break;
+      case '1':
+      case '01':
+        terminal.cellAttr.bold = true;
+        break;
+      case '2':
+      case '02':
+        terminal.cellAttr.faint = true;
+        break;
+      case '3':
+      case '03':
+        terminal.cellAttr.italic = true;
+        break;
+      case '4':
+      case '04':
+        terminal.cellAttr.underline = true;
+        break;
+      case '5':
+      case '05':
+        terminal.cellAttr.blink = true;
+        break;
+      case '7':
+      case '07':
+        terminal.cellAttr.inverse = true;
+        break;
+      case '8':
+      case '08':
+        terminal.cellAttr.invisible = true;
+        break;
+      case '21':
+        terminal.cellAttr.bold = false;
+        break;
+      case '22':
+        terminal.cellAttr.faint = false;
+        break;
+      case '23':
+        terminal.cellAttr.italic = false;
+        break;
+      case '24':
+        terminal.cellAttr.underline = false;
+        break;
+      case '25':
+        terminal.cellAttr.blink = false;
+        break;
+      case '27':
+        terminal.cellAttr.inverse = false;
+        break;
+      case '28':
+        terminal.cellAttr.invisible = false;
+        break;
+      case '29':
+        // not strikethrough
+        break;
+      case '39':
+        terminal.cellAttr.fgColor = terminal.colorScheme.foreground;
+        break;
+      case '30':
+        terminal.cellAttr.fgColor = terminal.colorScheme.black;
+        break;
+      case '31':
+        terminal.cellAttr.fgColor = terminal.colorScheme.red;
+        break;
+      case '32':
+        terminal.cellAttr.fgColor = terminal.colorScheme.green;
+        break;
+      case '33':
+        terminal.cellAttr.fgColor = terminal.colorScheme.yellow;
+        break;
+      case '34':
+        terminal.cellAttr.fgColor = terminal.colorScheme.blue;
+        break;
+      case '35':
+        terminal.cellAttr.fgColor = terminal.colorScheme.magenta;
+        break;
+      case '36':
+        terminal.cellAttr.fgColor = terminal.colorScheme.cyan;
+        break;
+      case '37':
+        terminal.cellAttr.fgColor = terminal.colorScheme.white;
+        break;
+      case '90':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightBlack;
+        break;
+      case '91':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightRed;
+        break;
+      case '92':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightGreen;
+        break;
+      case '93':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightYellow;
+        break;
+      case '94':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightBlue;
+        break;
+      case '95':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightMagenta;
+        break;
+      case '96':
+        terminal.cellAttr.fgColor = terminal.colorScheme.brightCyan;
+        break;
+      case '97':
+        terminal.cellAttr.fgColor = terminal.colorScheme.white;
+        break;
+      case '49':
+        terminal.cellAttr.bgColor = terminal.colorScheme.background;
+        break;
+      case '40':
+        terminal.cellAttr.bgColor = terminal.colorScheme.black;
+        break;
+      case '41':
+        terminal.cellAttr.bgColor = terminal.colorScheme.red;
+        break;
+      case '42':
+        terminal.cellAttr.bgColor = terminal.colorScheme.green;
+        break;
+      case '43':
+        terminal.cellAttr.bgColor = terminal.colorScheme.yellow;
+        break;
+      case '44':
+        terminal.cellAttr.bgColor = terminal.colorScheme.blue;
+        break;
+      case '45':
+        terminal.cellAttr.bgColor = terminal.colorScheme.magenta;
+        break;
+      case '46':
+        terminal.cellAttr.bgColor = terminal.colorScheme.cyan;
+        break;
+      case '47':
+        terminal.cellAttr.bgColor = terminal.colorScheme.white;
+        break;
+      case '100':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightBlack;
+        break;
+      case '101':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightRed;
+        break;
+      case '102':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightGreen;
+        break;
+      case '103':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightYellow;
+        break;
+      case '104':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightBlue;
+        break;
+      case '105':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightMagenta;
+        break;
+      case '106':
+        terminal.cellAttr.bgColor = terminal.colorScheme.brightCyan;
+        break;
+      case '107':
+        terminal.cellAttr.bgColor = terminal.colorScheme.white;
+        break;
+      case '38': // set foreground
+        final color = parseAnsiColour(params.sublist(i), terminal);
+        terminal.cellAttr.fgColor = color;
+        return;
+      case '48': // set background
+        final color = parseAnsiColour(params.sublist(i), terminal);
+        terminal.cellAttr.bgColor = color;
+        return;
+      default:
+        terminal.debug.onError('unknown SGR: $param');
+    }
+  }
+}
+
+CellColor parseAnsiColour(List<String> params, Terminal terminal) {
+  if (params.length > 2) {
+    switch (params[1]) {
+      case "5":
+        // 8 bit colour
+        final colNum = int.tryParse(params[2]);
+
+        if (colNum == null || colNum >= 256 || colNum < 0) {
+          return CellColor.empty();
+        }
+
+        return parse8BitSgrColour(colNum, terminal);
+
+      case "2":
+        if (params.length < 4) {
+          return CellColor.empty();
+        }
+
+        // 24 bit colour
+        if (params.length == 5) {
+          final r = int.tryParse(params[2]);
+          final g = int.tryParse(params[3]);
+          final b = int.tryParse(params[4]);
+
+          if (r == null || g == null || b == null) {
+            return CellColor.empty();
+          }
+
+          return CellColor.fromARGB(0xff, r, g, b);
+        }
+
+        if (params.length > 5) {
+          // ISO/IEC International Standard 8613-6
+          final r = int.tryParse(params[3]);
+          final g = int.tryParse(params[4]);
+          final b = int.tryParse(params[5]);
+
+          if (r == null || g == null || b == null) {
+            return CellColor.empty();
+          }
+
+          return CellColor.fromARGB(0xff, r, g, b);
+        }
+    }
+  }
+
+  return CellColor.empty();
+}
+
+const grayscaleColors = {
+  232: 0xff080808,
+  233: 0xff121212,
+  234: 0xff1c1c1c,
+  235: 0xff262626,
+  236: 0xff303030,
+  237: 0xff3a3a3a,
+  238: 0xff444444,
+  239: 0xff4e4e4e,
+  240: 0xff585858,
+  241: 0xff626262,
+  242: 0xff6c6c6c,
+  243: 0xff767676,
+  244: 0xff808080,
+  245: 0xff8a8a8a,
+  246: 0xff949494,
+  247: 0xff9e9e9e,
+  248: 0xffa8a8a8,
+  249: 0xffb2b2b2,
+  250: 0xffbcbcbc,
+  251: 0xffc6c6c6,
+  252: 0xffd0d0d0,
+  253: 0xffdadada,
+  254: 0xffe4e4e4,
+  255: 0xffeeeeee,
+};
+
+CellColor parse8BitSgrColour(int colNum, Terminal terminal) {
+  // https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
+
+  switch (colNum) {
+    case 0:
+      return terminal.colorScheme.black;
+    case 1:
+      return terminal.colorScheme.red;
+    case 2:
+      return terminal.colorScheme.green;
+    case 3:
+      return terminal.colorScheme.yellow;
+    case 4:
+      return terminal.colorScheme.blue;
+    case 5:
+      return terminal.colorScheme.magenta;
+    case 6:
+      return terminal.colorScheme.cyan;
+    case 7:
+      return terminal.colorScheme.white;
+    case 8:
+      return terminal.colorScheme.brightBlack;
+    case 9:
+      return terminal.colorScheme.brightRed;
+    case 10:
+      return terminal.colorScheme.brightGreen;
+    case 11:
+      return terminal.colorScheme.brightYellow;
+    case 12:
+      return terminal.colorScheme.brightBlue;
+    case 13:
+      return terminal.colorScheme.brightMagenta;
+    case 14:
+      return terminal.colorScheme.brightCyan;
+    case 15:
+      return terminal.colorScheme.white;
+  }
+
+  if (colNum < 232) {
+    var r = 0;
+    var g = 0;
+    var b = 0;
+
+    final index = colNum - 16;
+
+    for (var i = 0; i < index; i++) {
+      if (b == 0) {
+        b = 95;
+      } else if (b < 255) {
+        b += 40;
+      } else {
+        b = 0;
+        if (g == 0) {
+          g = 95;
+        } else if (g < 255) {
+          g += 40;
+        } else {
+          g = 0;
+          if (r == 0) {
+            r = 95;
+          } else if (r < 255) {
+            r += 40;
+          } else {
+            break;
+          }
+        }
+      }
+    }
+
+    return CellColor.fromARGB(0xff, r, g, b);
+  }
+
+  return CellColor(grayscaleColors[colNum.clamp(232, 255)]);
+}

+ 28 - 0
lib/terminal/tabs.dart

@@ -0,0 +1,28 @@
+class Tabs {
+  final _stops = <int>{};
+
+  void setAt(int index) {
+    _stops.add(index);
+  }
+
+  void clearAt(int index) {
+    _stops.remove(index);
+  }
+
+  void clearAll() {
+    _stops.clear();
+  }
+
+  bool isSetAt(int index) {
+    return _stops.contains(index);
+  }
+
+  void reset() {
+    clearAll();
+    const maxTabs = 1024;
+    const tabLength = 4;
+    for (var i = 0; i < maxTabs; i += tabLength) {
+      setAt(i);
+    }
+  }
+}

+ 427 - 0
lib/terminal/terminal.dart

@@ -0,0 +1,427 @@
+import 'dart:async';
+import 'dart:math' show max;
+
+import 'package:async/async.dart';
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/cell_attr.dart';
+import 'package:xterm/mouse/selection.dart';
+import 'package:xterm/color/color_default.dart';
+import 'package:xterm/input/keys.dart';
+import 'package:xterm/input/keytab/keytab.dart';
+import 'package:xterm/input/keytab/keytab_escape.dart';
+import 'package:xterm/input/keytab/keytab_record.dart';
+import 'package:xterm/mouse/mouse_mode.dart';
+import 'package:xterm/terminal/ansi.dart';
+import 'package:xterm/terminal/sbc.dart';
+import 'package:xterm/terminal/tabs.dart';
+import 'package:xterm/utli/debug_handler.dart';
+import 'package:xterm/utli/observable.dart';
+
+typedef TerminalInputHandler = void Function(String);
+typedef BellHandler = void Function();
+typedef TitleChangeHandler = void Function(String);
+typedef IconChangeHandler = void Function(String);
+
+class Terminal with Observable {
+  Terminal({
+    this.onInput,
+    this.onBell,
+    this.onTitleChange,
+    this.onIconChange,
+    int maxLines,
+  }) {
+    _maxLines = maxLines;
+
+    _mainBuffer = Buffer(this);
+    _altBuffer = Buffer(this);
+    _buffer = _mainBuffer;
+
+    _input = StreamController<int>();
+    _queue = StreamQueue<int>(_input.stream);
+
+    tabs.reset();
+
+    _processInput();
+    // _buffer.write('this is magic!');
+  }
+
+  bool _dirty = false;
+  bool get dirty {
+    if (_dirty) {
+      _dirty = false;
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  int _maxLines;
+  int get maxLines {
+    if (_maxLines == null) return null;
+    return max(viewHeight, _maxLines);
+  }
+
+  int _viewWidth = 80;
+  int _viewHeight = 25;
+
+  int get viewWidth => _viewWidth;
+  int get viewHeight => _viewHeight;
+
+  bool _originMode = false;
+  bool _replaceMode = false;
+  bool _lineFeed = true;
+  bool _screenMode = false; // DECSCNM (black on white background)
+  bool _autoWrapMode = true;
+  bool _bracketedPasteMode = false;
+
+  bool get originMode => _originMode;
+  bool get lineFeed => _lineFeed;
+  bool get newLineMode => !_lineFeed;
+
+  bool _showCursor = true;
+  bool _applicationCursorKeys = false;
+  bool _blinkingCursor = true;
+
+  bool get showCursor => _showCursor;
+  bool get applicationCursorKeys => _applicationCursorKeys;
+  bool get blinkingCursor => _blinkingCursor;
+
+  Buffer _buffer;
+  Buffer _mainBuffer;
+  Buffer _altBuffer;
+
+  StreamController<int> _input;
+  StreamQueue<int> _queue;
+
+  bool _slowMotion = false;
+  bool get slowMotion => _slowMotion;
+
+  MouseMode _mouseMode = MouseMode.none;
+  MouseMode get mouseMode => _mouseMode;
+
+  final colorScheme = defaultColorScheme;
+  var cellAttr = CellAttr(fgColor: defaultColorScheme.foreground);
+
+  final keytab = Keytab.defaultKeytab();
+  final selection = Selection();
+  final tabs = Tabs();
+  final debug = DebugHandler();
+
+  final TerminalInputHandler onInput;
+  final BellHandler onBell;
+  final TitleChangeHandler onTitleChange;
+  final IconChangeHandler onIconChange;
+
+  void close() {
+    _input.close();
+    _queue.cancel();
+  }
+
+  Buffer get buffer {
+    return _buffer;
+  }
+
+  int get cursorX => buffer.cursorX;
+  int get cursorY => buffer.cursorY;
+  int get scrollOffset => buffer.scrollOffset;
+
+  void write(String text) async {
+    for (var char in text.runes) {
+      writeChar(char);
+    }
+  }
+
+  void writeChar(int codePoint) {
+    _input.add(codePoint);
+  }
+
+  List<BufferLine> getVisibleLines() {
+    return _buffer.getVisibleLines();
+  }
+
+  void _processInput() async {
+    while (true) {
+      if (_slowMotion) {
+        await Future.delayed(Duration(milliseconds: 100));
+      }
+
+      const esc = 0x1b;
+      final char = await _queue.next;
+
+      if (char == esc) {
+        await ansiHandler(_queue, this);
+        refresh();
+        continue;
+      }
+
+      _processChar(char);
+    }
+  }
+
+  void _processChar(int codePoint) {
+    final sbcHandler = sbcHandlers[codePoint];
+
+    if (sbcHandler != null) {
+      debug.onSbc(codePoint);
+      sbcHandler(codePoint, this);
+    } else {
+      debug.onChar(codePoint);
+      _buffer.writeChar(codePoint);
+    }
+
+    refresh();
+  }
+
+  void refresh() {
+    _dirty = true;
+    notifyListeners();
+  }
+
+  void setSlowMotion(bool enabled) {
+    _slowMotion = enabled ?? _slowMotion;
+  }
+
+  void setOriginMode(bool enabled) {
+    _originMode = enabled ?? _originMode;
+    buffer.setPosition(0, 0);
+  }
+
+  void setScreenMode(bool enabled) {
+    _screenMode = true;
+  }
+
+  void setApplicationCursorKeys(bool enabled) {
+    _applicationCursorKeys = enabled ?? _applicationCursorKeys;
+  }
+
+  void setShowCursor(bool showCursor) {
+    _showCursor = showCursor ?? _showCursor;
+  }
+
+  void setBlinkingCursor(bool enabled) {
+    _blinkingCursor = enabled ?? _blinkingCursor;
+  }
+
+  void setAutoWrapMode(bool enabled) {
+    _autoWrapMode = enabled;
+  }
+
+  void setBracketedPasteMode(bool enabled) {
+    _bracketedPasteMode = enabled;
+  }
+
+  void setInsertMode() {
+    _replaceMode = false;
+  }
+
+  void setReplaceMode() {
+    _replaceMode = true;
+  }
+
+  void setNewLineMode() {
+    _lineFeed = false;
+  }
+
+  void setLineFeedMode() {
+    _lineFeed = true;
+  }
+
+  void setMouseMode(MouseMode mode) {
+    _mouseMode = mode ?? _mouseMode;
+  }
+
+  void useMainBuffer() {
+    _buffer = _mainBuffer;
+  }
+
+  void useAltBuffer() {
+    _buffer = _altBuffer;
+  }
+
+  bool isUsingMainBuffer() {
+    return _buffer == _mainBuffer;
+  }
+
+  bool isUsingAltBuffer() {
+    return _buffer == _altBuffer;
+  }
+
+  void resize(int width, int heigth) {
+    final cursorY = buffer.convertViewLineToRawLine(buffer.cursorY);
+
+    _viewWidth = max(width, 1);
+    _viewHeight = max(heigth, 1);
+
+    buffer.setCursorY(buffer.convertRawLineToViewLine(cursorY));
+
+    buffer.resetVerticalMargins();
+
+    if (buffer == _altBuffer) {
+      buffer.clearScrollback();
+    }
+  }
+
+  void input(
+    TerminalKey key, {
+    bool ctrl,
+    bool alt,
+    bool shift,
+    // bool meta,
+  }) {
+    if (onInput == null) {
+      return;
+    }
+
+    for (var record in keytab.records) {
+      if (record.key != key) {
+        continue;
+      }
+
+      if (record.ctrl != null && record.ctrl != ctrl) {
+        continue;
+      }
+
+      if (record.shift != null && record.shift != shift) {
+        continue;
+      }
+
+      if (record.alt != null && record.alt != alt) {
+        continue;
+      }
+
+      if (record.anyModifier == true &&
+          (ctrl != true && alt != true && shift != true)) {
+        continue;
+      }
+
+      if (record.anyModifier == false &&
+          !(ctrl != true && alt != true && shift != true)) {
+        continue;
+      }
+
+      if (record.appScreen != null && record.appScreen != isUsingAltBuffer()) {
+        continue;
+      }
+
+      if (record.newLine != null && record.newLine != newLineMode) {
+        continue;
+      }
+
+      if (record.appCursorKeys != null &&
+          record.appCursorKeys != applicationCursorKeys) {
+        continue;
+      }
+
+      // TODO: support VT52
+      if (record.ansi == false) {
+        continue;
+      }
+
+      if (record.action.type == KeytabActionType.input) {
+        debug.onMsg('input: ${record.action.value}');
+        final input = keytabUnescape(record.action.value);
+        onInput(input);
+        return;
+      }
+    }
+
+    if (ctrl) {
+      if (key.index >= TerminalKey.keyA.index &&
+          key.index <= TerminalKey.keyZ.index) {
+        final input = key.index - TerminalKey.keyA.index + 1;
+        onInput(String.fromCharCode(input));
+        return;
+      }
+    }
+
+    if (alt) {
+      if (key.index >= TerminalKey.keyA.index &&
+          key.index <= TerminalKey.keyZ.index) {
+        final input = [0x1b, key.index - TerminalKey.keyA.index + 65];
+        onInput(String.fromCharCodes(input));
+        return;
+      }
+    }
+  }
+
+  String getSelectedText() {
+    if (selection.isEmpty) {
+      return '';
+    }
+
+    final builder = StringBuffer();
+
+    for (var row = selection.start.y; row <= selection.end.y; row++) {
+      if (row >= buffer.height) {
+        break;
+      }
+
+      final line = buffer.lines[row];
+
+      var xStart = 0;
+      var xEnd = viewWidth - 1;
+
+      if (row == selection.start.y) {
+        xStart = selection.start.x;
+      } else if (!line.isWrapped) {
+        builder.write("\n");
+      }
+
+      if (row == selection.end.y) {
+        xEnd = selection.end.x;
+      }
+
+      for (var col = xStart; col <= xEnd; col++) {
+        if (col >= line.length) {
+          break;
+        }
+
+        final cell = line.getCell(col);
+
+        if (cell.width == 0) {
+          continue;
+        }
+
+        var char = line.getCell(col).codePoint;
+
+        if (char == null || char == 0x00) {
+          const blank = 32;
+          char = blank;
+        }
+
+        builder.writeCharCode(char);
+      }
+    }
+
+    return builder.toString();
+  }
+
+  int get _tabIndexFromCursor {
+    var index = buffer.cursorX;
+
+    if (buffer.cursorX == viewWidth) {
+      index = 0;
+    }
+
+    return index;
+  }
+
+  void tabSetAtCursor() {
+    tabs.setAt(_tabIndexFromCursor);
+  }
+
+  void tabClearAtCursor() {
+    tabs.clearAt(_tabIndexFromCursor);
+  }
+
+  void tab() {
+    while (buffer.cursorX < viewWidth) {
+      buffer.write(' ');
+
+      if (tabs.isSetAt(buffer.cursorX)) {
+        break;
+      }
+    }
+  }
+}

+ 25 - 0
lib/utli/ansi_color.dart

@@ -0,0 +1,25 @@
+class AnsiColor {
+  static String red(Object text) {
+    return '\x1b[31m$text\x1b[39m';
+  }
+
+  static String green(Object text) {
+    return '\x1b[32m$text\x1b[39m';
+  }
+
+  static String yellow(Object text) {
+    return '\x1b[33m$text\x1b[39m';
+  }
+
+  static String blue(Object text) {
+    return '\x1b[34m$text\x1b[39m';
+  }
+
+  static String magenta(Object text) {
+    return '\x1b[35m$text\x1b[39m';
+  }
+
+  static String cyan(Object text) {
+    return '\x1b[36m$text\x1b[39m';
+  }
+}

+ 66 - 0
lib/utli/debug_handler.dart

@@ -0,0 +1,66 @@
+import 'package:convert/convert.dart';
+import 'package:xterm/terminal/csi.dart';
+import 'package:xterm/utli/ansi_color.dart';
+
+class DebugHandler {
+  final _buffer = StringBuffer();
+
+  var _enabled = false;
+
+  void enable([bool enabled = true]) {
+    _enabled = enabled ?? _enabled;
+  }
+
+  void _checkBuffer() {
+    if (!_enabled) return;
+
+    if (_buffer.isNotEmpty) {
+      print(AnsiColor.cyan('┤') + _buffer.toString() + AnsiColor.cyan('├'));
+      _buffer.clear();
+    }
+  }
+
+  void onCsi(CSI csi) {
+    if (!_enabled) return;
+    _checkBuffer();
+    print(AnsiColor.green('<CSI $csi>'));
+  }
+
+  void onEsc(String charAfterEsc) {
+    if (!_enabled) return;
+    _checkBuffer();
+    print(AnsiColor.green('<ESC $charAfterEsc>'));
+  }
+
+  void onOsc(List<String> params) {
+    if (!_enabled) return;
+    _checkBuffer();
+    print(AnsiColor.yellow('<OSC $params>'));
+  }
+
+  void onSbc(int codePoint) {
+    if (!_enabled) return;
+    _checkBuffer();
+    print(AnsiColor.magenta('<SBC ${hex.encode([codePoint])}>'));
+  }
+
+  void onChar(int codePoint) {
+    if (!_enabled) return;
+    _buffer.writeCharCode(codePoint);
+  }
+
+  void onMetrics(String metrics) {
+    if (!_enabled) return;
+    print(AnsiColor.blue('<MRC $metrics>'));
+  }
+
+  void onError(String error) {
+    if (!_enabled) return;
+    print(AnsiColor.red('<ERR $error>'));
+  }
+
+  void onMsg(Object msg) {
+    if (!_enabled) return;
+    print(AnsiColor.green('<MSG $msg>'));
+  }
+}

+ 19 - 0
lib/utli/observable.dart

@@ -0,0 +1,19 @@
+typedef _VoidCallback = void Function();
+
+mixin Observable {
+  final listeners = <_VoidCallback>{};
+
+  void addListener(_VoidCallback listener) {
+    listeners.add(listener);
+  }
+
+  void removeListener(_VoidCallback listener) {
+    listeners.remove(listener);
+  }
+
+  void notifyListeners() {
+    for (var listener in listeners) {
+      listener();
+    }
+  }
+}

+ 6 - 0
lib/utli/scroll_range.dart

@@ -0,0 +1,6 @@
+class ScrollRange {
+  ScrollRange(this.top, this.bottom);
+
+  final int top;
+  final int bottom;
+}

+ 511 - 0
lib/utli/unicode_v11.dart

@@ -0,0 +1,511 @@
+import 'dart:typed_data';
+
+const BMP_COMBINING = [
+  [0x0300, 0x036F],
+  [0x0483, 0x0489],
+  [0x0591, 0x05BD],
+  [0x05BF, 0x05BF],
+  [0x05C1, 0x05C2],
+  [0x05C4, 0x05C5],
+  [0x05C7, 0x05C7],
+  [0x0600, 0x0605],
+  [0x0610, 0x061A],
+  [0x061C, 0x061C],
+  [0x064B, 0x065F],
+  [0x0670, 0x0670],
+  [0x06D6, 0x06DD],
+  [0x06DF, 0x06E4],
+  [0x06E7, 0x06E8],
+  [0x06EA, 0x06ED],
+  [0x070F, 0x070F],
+  [0x0711, 0x0711],
+  [0x0730, 0x074A],
+  [0x07A6, 0x07B0],
+  [0x07EB, 0x07F3],
+  [0x07FD, 0x07FD],
+  [0x0816, 0x0819],
+  [0x081B, 0x0823],
+  [0x0825, 0x0827],
+  [0x0829, 0x082D],
+  [0x0859, 0x085B],
+  [0x08D3, 0x0902],
+  [0x093A, 0x093A],
+  [0x093C, 0x093C],
+  [0x0941, 0x0948],
+  [0x094D, 0x094D],
+  [0x0951, 0x0957],
+  [0x0962, 0x0963],
+  [0x0981, 0x0981],
+  [0x09BC, 0x09BC],
+  [0x09C1, 0x09C4],
+  [0x09CD, 0x09CD],
+  [0x09E2, 0x09E3],
+  [0x09FE, 0x09FE],
+  [0x0A01, 0x0A02],
+  [0x0A3C, 0x0A3C],
+  [0x0A41, 0x0A42],
+  [0x0A47, 0x0A48],
+  [0x0A4B, 0x0A4D],
+  [0x0A51, 0x0A51],
+  [0x0A70, 0x0A71],
+  [0x0A75, 0x0A75],
+  [0x0A81, 0x0A82],
+  [0x0ABC, 0x0ABC],
+  [0x0AC1, 0x0AC5],
+  [0x0AC7, 0x0AC8],
+  [0x0ACD, 0x0ACD],
+  [0x0AE2, 0x0AE3],
+  [0x0AFA, 0x0AFF],
+  [0x0B01, 0x0B01],
+  [0x0B3C, 0x0B3C],
+  [0x0B3F, 0x0B3F],
+  [0x0B41, 0x0B44],
+  [0x0B4D, 0x0B4D],
+  [0x0B56, 0x0B56],
+  [0x0B62, 0x0B63],
+  [0x0B82, 0x0B82],
+  [0x0BC0, 0x0BC0],
+  [0x0BCD, 0x0BCD],
+  [0x0C00, 0x0C00],
+  [0x0C04, 0x0C04],
+  [0x0C3E, 0x0C40],
+  [0x0C46, 0x0C48],
+  [0x0C4A, 0x0C4D],
+  [0x0C55, 0x0C56],
+  [0x0C62, 0x0C63],
+  [0x0C81, 0x0C81],
+  [0x0CBC, 0x0CBC],
+  [0x0CBF, 0x0CBF],
+  [0x0CC6, 0x0CC6],
+  [0x0CCC, 0x0CCD],
+  [0x0CE2, 0x0CE3],
+  [0x0D00, 0x0D01],
+  [0x0D3B, 0x0D3C],
+  [0x0D41, 0x0D44],
+  [0x0D4D, 0x0D4D],
+  [0x0D62, 0x0D63],
+  [0x0DCA, 0x0DCA],
+  [0x0DD2, 0x0DD4],
+  [0x0DD6, 0x0DD6],
+  [0x0E31, 0x0E31],
+  [0x0E34, 0x0E3A],
+  [0x0E47, 0x0E4E],
+  [0x0EB1, 0x0EB1],
+  [0x0EB4, 0x0EBC],
+  [0x0EC8, 0x0ECD],
+  [0x0F18, 0x0F19],
+  [0x0F35, 0x0F35],
+  [0x0F37, 0x0F37],
+  [0x0F39, 0x0F39],
+  [0x0F71, 0x0F7E],
+  [0x0F80, 0x0F84],
+  [0x0F86, 0x0F87],
+  [0x0F8D, 0x0F97],
+  [0x0F99, 0x0FBC],
+  [0x0FC6, 0x0FC6],
+  [0x102D, 0x1030],
+  [0x1032, 0x1037],
+  [0x1039, 0x103A],
+  [0x103D, 0x103E],
+  [0x1058, 0x1059],
+  [0x105E, 0x1060],
+  [0x1071, 0x1074],
+  [0x1082, 0x1082],
+  [0x1085, 0x1086],
+  [0x108D, 0x108D],
+  [0x109D, 0x109D],
+  [0x1160, 0x11FF],
+  [0x135D, 0x135F],
+  [0x1712, 0x1714],
+  [0x1732, 0x1734],
+  [0x1752, 0x1753],
+  [0x1772, 0x1773],
+  [0x17B4, 0x17B5],
+  [0x17B7, 0x17BD],
+  [0x17C6, 0x17C6],
+  [0x17C9, 0x17D3],
+  [0x17DD, 0x17DD],
+  [0x180B, 0x180E],
+  [0x1885, 0x1886],
+  [0x18A9, 0x18A9],
+  [0x1920, 0x1922],
+  [0x1927, 0x1928],
+  [0x1932, 0x1932],
+  [0x1939, 0x193B],
+  [0x1A17, 0x1A18],
+  [0x1A1B, 0x1A1B],
+  [0x1A56, 0x1A56],
+  [0x1A58, 0x1A5E],
+  [0x1A60, 0x1A60],
+  [0x1A62, 0x1A62],
+  [0x1A65, 0x1A6C],
+  [0x1A73, 0x1A7C],
+  [0x1A7F, 0x1A7F],
+  [0x1AB0, 0x1ABE],
+  [0x1B00, 0x1B03],
+  [0x1B34, 0x1B34],
+  [0x1B36, 0x1B3A],
+  [0x1B3C, 0x1B3C],
+  [0x1B42, 0x1B42],
+  [0x1B6B, 0x1B73],
+  [0x1B80, 0x1B81],
+  [0x1BA2, 0x1BA5],
+  [0x1BA8, 0x1BA9],
+  [0x1BAB, 0x1BAD],
+  [0x1BE6, 0x1BE6],
+  [0x1BE8, 0x1BE9],
+  [0x1BED, 0x1BED],
+  [0x1BEF, 0x1BF1],
+  [0x1C2C, 0x1C33],
+  [0x1C36, 0x1C37],
+  [0x1CD0, 0x1CD2],
+  [0x1CD4, 0x1CE0],
+  [0x1CE2, 0x1CE8],
+  [0x1CED, 0x1CED],
+  [0x1CF4, 0x1CF4],
+  [0x1CF8, 0x1CF9],
+  [0x1DC0, 0x1DF9],
+  [0x1DFB, 0x1DFF],
+  [0x200B, 0x200F],
+  [0x202A, 0x202E],
+  [0x2060, 0x2064],
+  [0x2066, 0x206F],
+  [0x20D0, 0x20F0],
+  [0x2CEF, 0x2CF1],
+  [0x2D7F, 0x2D7F],
+  [0x2DE0, 0x2DFF],
+  [0x302A, 0x302D],
+  [0x3099, 0x309A],
+  [0xA66F, 0xA672],
+  [0xA674, 0xA67D],
+  [0xA69E, 0xA69F],
+  [0xA6F0, 0xA6F1],
+  [0xA802, 0xA802],
+  [0xA806, 0xA806],
+  [0xA80B, 0xA80B],
+  [0xA825, 0xA826],
+  [0xA8C4, 0xA8C5],
+  [0xA8E0, 0xA8F1],
+  [0xA8FF, 0xA8FF],
+  [0xA926, 0xA92D],
+  [0xA947, 0xA951],
+  [0xA980, 0xA982],
+  [0xA9B3, 0xA9B3],
+  [0xA9B6, 0xA9B9],
+  [0xA9BC, 0xA9BD],
+  [0xA9E5, 0xA9E5],
+  [0xAA29, 0xAA2E],
+  [0xAA31, 0xAA32],
+  [0xAA35, 0xAA36],
+  [0xAA43, 0xAA43],
+  [0xAA4C, 0xAA4C],
+  [0xAA7C, 0xAA7C],
+  [0xAAB0, 0xAAB0],
+  [0xAAB2, 0xAAB4],
+  [0xAAB7, 0xAAB8],
+  [0xAABE, 0xAABF],
+  [0xAAC1, 0xAAC1],
+  [0xAAEC, 0xAAED],
+  [0xAAF6, 0xAAF6],
+  [0xABE5, 0xABE5],
+  [0xABE8, 0xABE8],
+  [0xABED, 0xABED],
+  [0xFB1E, 0xFB1E],
+  [0xFE00, 0xFE0F],
+  [0xFE20, 0xFE2F],
+  [0xFEFF, 0xFEFF],
+  [0xFFF9, 0xFFFB]
+];
+
+const HIGH_COMBINING = [
+  [0x101FD, 0x101FD],
+  [0x102E0, 0x102E0],
+  [0x10376, 0x1037A],
+  [0x10A01, 0x10A03],
+  [0x10A05, 0x10A06],
+  [0x10A0C, 0x10A0F],
+  [0x10A38, 0x10A3A],
+  [0x10A3F, 0x10A3F],
+  [0x10AE5, 0x10AE6],
+  [0x10D24, 0x10D27],
+  [0x10F46, 0x10F50],
+  [0x11001, 0x11001],
+  [0x11038, 0x11046],
+  [0x1107F, 0x11081],
+  [0x110B3, 0x110B6],
+  [0x110B9, 0x110BA],
+  [0x110BD, 0x110BD],
+  [0x110CD, 0x110CD],
+  [0x11100, 0x11102],
+  [0x11127, 0x1112B],
+  [0x1112D, 0x11134],
+  [0x11173, 0x11173],
+  [0x11180, 0x11181],
+  [0x111B6, 0x111BE],
+  [0x111C9, 0x111CC],
+  [0x1122F, 0x11231],
+  [0x11234, 0x11234],
+  [0x11236, 0x11237],
+  [0x1123E, 0x1123E],
+  [0x112DF, 0x112DF],
+  [0x112E3, 0x112EA],
+  [0x11300, 0x11301],
+  [0x1133B, 0x1133C],
+  [0x11340, 0x11340],
+  [0x11366, 0x1136C],
+  [0x11370, 0x11374],
+  [0x11438, 0x1143F],
+  [0x11442, 0x11444],
+  [0x11446, 0x11446],
+  [0x1145E, 0x1145E],
+  [0x114B3, 0x114B8],
+  [0x114BA, 0x114BA],
+  [0x114BF, 0x114C0],
+  [0x114C2, 0x114C3],
+  [0x115B2, 0x115B5],
+  [0x115BC, 0x115BD],
+  [0x115BF, 0x115C0],
+  [0x115DC, 0x115DD],
+  [0x11633, 0x1163A],
+  [0x1163D, 0x1163D],
+  [0x1163F, 0x11640],
+  [0x116AB, 0x116AB],
+  [0x116AD, 0x116AD],
+  [0x116B0, 0x116B5],
+  [0x116B7, 0x116B7],
+  [0x1171D, 0x1171F],
+  [0x11722, 0x11725],
+  [0x11727, 0x1172B],
+  [0x1182F, 0x11837],
+  [0x11839, 0x1183A],
+  [0x119D4, 0x119D7],
+  [0x119DA, 0x119DB],
+  [0x119E0, 0x119E0],
+  [0x11A01, 0x11A0A],
+  [0x11A33, 0x11A38],
+  [0x11A3B, 0x11A3E],
+  [0x11A47, 0x11A47],
+  [0x11A51, 0x11A56],
+  [0x11A59, 0x11A5B],
+  [0x11A8A, 0x11A96],
+  [0x11A98, 0x11A99],
+  [0x11C30, 0x11C36],
+  [0x11C38, 0x11C3D],
+  [0x11C3F, 0x11C3F],
+  [0x11C92, 0x11CA7],
+  [0x11CAA, 0x11CB0],
+  [0x11CB2, 0x11CB3],
+  [0x11CB5, 0x11CB6],
+  [0x11D31, 0x11D36],
+  [0x11D3A, 0x11D3A],
+  [0x11D3C, 0x11D3D],
+  [0x11D3F, 0x11D45],
+  [0x11D47, 0x11D47],
+  [0x11D90, 0x11D91],
+  [0x11D95, 0x11D95],
+  [0x11D97, 0x11D97],
+  [0x11EF3, 0x11EF4],
+  [0x13430, 0x13438],
+  [0x16AF0, 0x16AF4],
+  [0x16B30, 0x16B36],
+  [0x16F4F, 0x16F4F],
+  [0x16F8F, 0x16F92],
+  [0x1BC9D, 0x1BC9E],
+  [0x1BCA0, 0x1BCA3],
+  [0x1D167, 0x1D169],
+  [0x1D173, 0x1D182],
+  [0x1D185, 0x1D18B],
+  [0x1D1AA, 0x1D1AD],
+  [0x1D242, 0x1D244],
+  [0x1DA00, 0x1DA36],
+  [0x1DA3B, 0x1DA6C],
+  [0x1DA75, 0x1DA75],
+  [0x1DA84, 0x1DA84],
+  [0x1DA9B, 0x1DA9F],
+  [0x1DAA1, 0x1DAAF],
+  [0x1E000, 0x1E006],
+  [0x1E008, 0x1E018],
+  [0x1E01B, 0x1E021],
+  [0x1E023, 0x1E024],
+  [0x1E026, 0x1E02A],
+  [0x1E130, 0x1E136],
+  [0x1E2EC, 0x1E2EF],
+  [0x1E8D0, 0x1E8D6],
+  [0x1E944, 0x1E94A],
+  [0xE0001, 0xE0001],
+  [0xE0020, 0xE007F],
+  [0xE0100, 0xE01EF]
+];
+
+const BMP_WIDE = [
+  [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, 0x3029],
+  [0x302E, 0x303E],
+  [0x3041, 0x3096],
+  [0x309B, 0x30FF],
+  [0x3105, 0x312F],
+  [0x3131, 0x318E],
+  [0x3190, 0x31BA],
+  [0x31C0, 0x31E3],
+  [0x31F0, 0x321E],
+  [0x3220, 0x3247],
+  [0x3250, 0x4DBF],
+  [0x4E00, 0xA48C],
+  [0xA490, 0xA4C6],
+  [0xA960, 0xA97C],
+  [0xAC00, 0xD7A3],
+  [0xF900, 0xFAFF],
+  [0xFE10, 0xFE19],
+  [0xFE30, 0xFE52],
+  [0xFE54, 0xFE66],
+  [0xFE68, 0xFE6B],
+  [0xFF01, 0xFF60],
+  [0xFFE0, 0xFFE6]
+];
+
+const HIGH_WIDE = [
+  [0x16FE0, 0x16FE3],
+  [0x17000, 0x187F7],
+  [0x18800, 0x18AF2],
+  [0x1B000, 0x1B11E],
+  [0x1B150, 0x1B152],
+  [0x1B164, 0x1B167],
+  [0x1B170, 0x1B2FB],
+  [0x1F004, 0x1F004],
+  [0x1F0CF, 0x1F0CF],
+  [0x1F18E, 0x1F18E],
+  [0x1F191, 0x1F19A],
+  [0x1F200, 0x1F202],
+  [0x1F210, 0x1F23B],
+  [0x1F240, 0x1F248],
+  [0x1F250, 0x1F251],
+  [0x1F260, 0x1F265],
+  [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],
+  [0x1F6D5, 0x1F6D5],
+  [0x1F6EB, 0x1F6EC],
+  [0x1F6F4, 0x1F6FA],
+  [0x1F7E0, 0x1F7EB],
+  [0x1F90D, 0x1F971],
+  [0x1F973, 0x1F976],
+  [0x1F97A, 0x1F9A2],
+  [0x1F9A5, 0x1F9AA],
+  [0x1F9AE, 0x1F9CA],
+  [0x1F9CD, 0x1F9FF],
+  [0x1FA70, 0x1FA73],
+  [0x1FA78, 0x1FA7A],
+  [0x1FA80, 0x1FA82],
+  [0x1FA90, 0x1FA95],
+  [0x20000, 0x2FFFD],
+  [0x30000, 0x3FFFD]
+];
+
+Uint8List table;
+
+bool bisearch(int ucs, List<List<int>> data) {
+  var min = 0;
+  var max = data.length - 1;
+  var mid;
+  if (ucs < data[0][0] || ucs > data[max][1]) {
+    return false;
+  }
+  while (max >= min) {
+    mid = (min + max) >> 1;
+    if (ucs > data[mid][1]) {
+      min = mid + 1;
+    } else if (ucs < data[mid][0]) {
+      max = mid - 1;
+    } else {
+      return true;
+    }
+  }
+  return false;
+}
+
+class UnicodeV11 {
+  final version = '11';
+
+  UnicodeV11() {
+    if (table == null) {
+      table = Uint8List(65536);
+      table.fillRange(0, table.length, 1);
+      table[0] = 0;
+      table.fillRange(1, 32, 0);
+      table.fillRange(0x7f, 0xa0, 0);
+      for (var r = 0; r < BMP_COMBINING.length; ++r) {
+        table.fillRange(BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1, 0);
+      }
+      for (var r = 0; r < BMP_WIDE.length; ++r) {
+        table.fillRange(BMP_WIDE[r][0], BMP_WIDE[r][1] + 1, 2);
+      }
+    }
+  }
+
+  int wcwidth(int codePoint) {
+    if (codePoint < 32) return 0;
+    if (codePoint < 127) return 1;
+    if (codePoint < 65536) return table[codePoint];
+    if (bisearch(codePoint, HIGH_COMBINING)) return 0;
+    if (bisearch(codePoint, HIGH_WIDE)) return 2;
+    return 1;
+  }
+}
+
+final unicodeV11 = UnicodeV11();

+ 4 - 0
lib/xterm.dart

@@ -0,0 +1,4 @@
+library xterm;
+
+export 'terminal/terminal.dart';
+export 'frontend/terminal_view.dart';

BIN
media/demo-dialog.png


BIN
media/demo-htop.png


BIN
media/demo-shell.png


BIN
media/demo-vim.png


+ 161 - 0
pubspec.lock

@@ -0,0 +1,161 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+  async:
+    dependency: "direct main"
+    description:
+      name: async
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.4.2"
+  boolean_selector:
+    dependency: transitive
+    description:
+      name: boolean_selector
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.0"
+  characters:
+    dependency: transitive
+    description:
+      name: characters
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.0"
+  charcode:
+    dependency: transitive
+    description:
+      name: charcode
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.3"
+  clock:
+    dependency: transitive
+    description:
+      name: clock
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.1"
+  collection:
+    dependency: transitive
+    description:
+      name: collection
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.14.13"
+  convert:
+    dependency: "direct main"
+    description:
+      name: convert
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.1"
+  fake_async:
+    dependency: transitive
+    description:
+      name: fake_async
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.0"
+  flutter:
+    dependency: "direct main"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_test:
+    dependency: "direct dev"
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  matcher:
+    dependency: transitive
+    description:
+      name: matcher
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.12.8"
+  meta:
+    dependency: "direct main"
+    description:
+      name: meta
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.8"
+  path:
+    dependency: transitive
+    description:
+      name: path
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.7.0"
+  quiver:
+    dependency: "direct main"
+    description:
+      name: quiver
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.1.3"
+  sky_engine:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.99"
+  source_span:
+    dependency: transitive
+    description:
+      name: source_span
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.7.0"
+  stack_trace:
+    dependency: transitive
+    description:
+      name: stack_trace
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.9.5"
+  stream_channel:
+    dependency: transitive
+    description:
+      name: stream_channel
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.0"
+  string_scanner:
+    dependency: transitive
+    description:
+      name: string_scanner
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.0.5"
+  term_glyph:
+    dependency: transitive
+    description:
+      name: term_glyph
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.1.0"
+  test_api:
+    dependency: transitive
+    description:
+      name: test_api
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "0.2.17"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "1.2.0"
+  vector_math:
+    dependency: transitive
+    description:
+      name: vector_math
+      url: "https://pub.flutter-io.cn"
+    source: hosted
+    version: "2.0.8"
+sdks:
+  dart: ">=2.9.0-14.0.dev <3.0.0"
+  flutter: ">=1.17.0 <2.0.0"

+ 56 - 0
pubspec.yaml

@@ -0,0 +1,56 @@
+name: xterm
+description: Xterm.dart is a fast and fully-featured terminal emulator for Flutter, with support for mobile and desktop platforms.
+version: 0.0.1
+author: xuty
+homepage: https://github.com/TerminalStudio/xterm.dart
+
+environment:
+  sdk: ">=2.7.0 <3.0.0"
+  flutter: ">=1.17.0 <2.0.0"
+
+dependencies:
+  convert: ^2.1.1
+  async: ^2.4.1
+  meta: ^1.1.8
+  quiver: ^2.1.3
+  flutter:
+    sdk: flutter
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+  # To add assets to your package, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+  #
+  # For details regarding assets in packages, see
+  # https://flutter.dev/assets-and-images/#from-packages
+  #
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+  # To add custom fonts to your package, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts in packages, see
+  # https://flutter.dev/custom-fonts/#from-packages

+ 13 - 0
test/xterm_test.dart

@@ -0,0 +1,13 @@
+// import 'package:flutter_test/flutter_test.dart';
+
+// import 'package:xterm/xterm.dart';
+
+// void main() {
+//   test('adds one to input values', () {
+//     final calculator = Calculator();
+//     expect(calculator.addOne(2), 3);
+//     expect(calculator.addOne(-7), -6);
+//     expect(calculator.addOne(0), 1);
+//     expect(() => calculator.addOne(null), throwsNoSuchMethodError);
+//   });
+// }