Procházet zdrojové kódy

Merge branch 'master' of https://github.com/TerminalStudio/xterm.dart into HEAD

xuty před 5 roky
rodič
revize
bdbe81bed0

+ 21 - 21
example/pubspec.lock

@@ -5,63 +5,63 @@ packages:
     dependency: transitive
     description:
       name: async
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.5.0-nullsafety"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety"
   characters:
     dependency: transitive
     description:
       name: characters
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety.2"
   charcode:
     dependency: transitive
     description:
       name: charcode
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.2.0-nullsafety"
   clock:
     dependency: transitive
     description:
       name: clock
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   collection:
     dependency: transitive
     description:
       name: collection
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.15.0-nullsafety.2"
   convert:
     dependency: transitive
     description:
       name: convert
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.1"
   cupertino_icons:
     dependency: "direct main"
     description:
       name: cupertino_icons
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "0.1.3"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   flutter:
@@ -78,28 +78,28 @@ packages:
     dependency: transitive
     description:
       name: matcher
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "0.12.10-nullsafety"
   meta:
     dependency: transitive
     description:
       name: meta
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.3.0-nullsafety.2"
   path:
     dependency: transitive
     description:
       name: path
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.8.0-nullsafety"
   quiver:
     dependency: transitive
     description:
       name: quiver
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.3"
   sky_engine:
@@ -111,56 +111,56 @@ packages:
     dependency: transitive
     description:
       name: source_span
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.8.0-nullsafety.2"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.10.0-nullsafety"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.2.0-nullsafety"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "0.2.19-nullsafety"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.3.0-nullsafety.2"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety.2"
   xterm:

+ 26 - 17
lib/buffer/buffer.dart

@@ -54,7 +54,7 @@ class Buffer {
     final cell = line.getCell(_cursorX);
     cell.setCodePoint(codePoint);
     cell.setWidth(cellWidth);
-    cell.setAttr(terminal.cellAttr.copy());
+    cell.setAttr(terminal.cellAttr.value);
 
     if (_cursorX < terminal.viewWidth) {
       _cursorX++;
@@ -128,7 +128,7 @@ class Buffer {
     final result = <BufferLine>[];
 
     for (var i = height - terminal.viewHeight; i < height; i++) {
-      final y = i - scrollOffset;
+      final y = i - scrollOffsetFromBottom;
       if (y >= 0 && y < height) {
         result.add(lines[y]);
       }
@@ -141,7 +141,7 @@ class Buffer {
     eraseLineFromCursor();
 
     for (var i = _cursorY + 1; i < terminal.viewHeight; i++) {
-      getViewLine(i).erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+      getViewLine(i).erase(terminal.cellAttr.value, 0, terminal.viewWidth);
     }
   }
 
@@ -149,36 +149,36 @@ class Buffer {
     eraseLineToCursor();
 
     for (var i = 0; i < _cursorY; i++) {
-      getViewLine(i).erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+      getViewLine(i).erase(terminal.cellAttr.value, 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);
+      line.erase(terminal.cellAttr.value, 0, terminal.viewWidth);
     }
   }
 
   void eraseLineFromCursor() {
-    currentLine.erase(terminal.cellAttr.copy(), _cursorX, terminal.viewWidth);
+    currentLine.erase(terminal.cellAttr.value, _cursorX, terminal.viewWidth);
   }
 
   void eraseLineToCursor() {
-    currentLine.erase(terminal.cellAttr.copy(), 0, _cursorX);
+    currentLine.erase(terminal.cellAttr.value, 0, _cursorX);
   }
 
   void eraseLine() {
-    currentLine.erase(terminal.cellAttr.copy(), 0, terminal.viewWidth);
+    currentLine.erase(terminal.cellAttr.value, 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()));
+        currentLine.add(Cell(attr: terminal.cellAttr.value));
       } else {
-        currentLine.getCell(i).erase(terminal.cellAttr.copy());
+        currentLine.getCell(i).erase(terminal.cellAttr.value);
       }
     }
   }
@@ -310,27 +310,36 @@ class Buffer {
     setPosition(cursorX, cursorY);
   }
 
-  int get scrollOffset {
+  int get scrollOffsetFromBottom {
     return _scrollLinesFromBottom;
   }
 
-  void setScrollOffset(int offset) {
+  int get scrollOffsetFromTop {
+    return terminal.invisibleHeight - scrollOffsetFromBottom;
+  }
+
+  void setScrollOffsetFromBottom(int offset) {
     if (height < terminal.viewHeight) return;
     final maxOffset = height - terminal.viewHeight;
     _scrollLinesFromBottom = offset.clamp(0, maxOffset);
     terminal.refresh();
   }
 
+  void setScrollOffsetFromTop(int offset) {
+    final bottomOffset = terminal.invisibleHeight - offset;
+    setScrollOffsetFromBottom(bottomOffset);
+  }
+
   void screenScrollUp(int lines) {
-    setScrollOffset(scrollOffset + lines);
+    setScrollOffsetFromBottom(scrollOffsetFromBottom + lines);
   }
 
   void screenScrollDown(int lines) {
-    setScrollOffset(scrollOffset - lines);
+    setScrollOffsetFromBottom(scrollOffsetFromBottom - lines);
   }
 
   void saveCursor() {
-    _savedCellAttr = terminal.cellAttr.copy();
+    _savedCellAttr = terminal.cellAttr.value;
     _savedCursorX = _cursorX;
     _savedCursorY = _cursorY;
     charset.save();
@@ -338,7 +347,7 @@ class Buffer {
 
   void restoreCursor() {
     if (_savedCellAttr != null) {
-      terminal.cellAttr = _savedCellAttr.copy();
+      terminal.cellAttr.use(_savedCellAttr);
     }
 
     if (_savedCursorX != null) {
@@ -394,7 +403,7 @@ class Buffer {
 
   void insertBlankCharacters(int count) {
     for (var i = 0; i < count; i++) {
-      final cell = Cell(attr: terminal.cellAttr.copy());
+      final cell = Cell(attr: terminal.cellAttr.value);
       currentLine.insert(_cursorX + i, cell);
     }
   }

+ 83 - 57
lib/buffer/cell_attr.dart

@@ -1,9 +1,9 @@
-import 'package:meta/meta.dart';
-import 'package:xterm/buffer/cell_color.dart';
+import 'package:xterm/theme/terminal_color.dart';
+import 'package:xterm/utli/hash_values.dart';
 
 class CellAttr {
   CellAttr({
-    @required this.fgColor,
+    this.fgColor,
     this.bgColor,
     this.bold = false,
     this.faint = false,
@@ -12,57 +12,73 @@ class CellAttr {
     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;
-  }
+  }) : hashCode = hashValues(
+          fgColor,
+          bgColor,
+          bold,
+          faint,
+          italic,
+          underline,
+          blink,
+          inverse,
+          invisible,
+        );
+
+  final TerminalColor fgColor;
+  final TerminalColor bgColor;
+  final bool bold;
+  final bool faint;
+  final bool italic;
+  final bool underline;
+  final bool blink;
+  final bool inverse;
+  final bool invisible;
+
+  @override
+  final int hashCode;
+
+  @override
+  bool operator ==(Object other) => other.hashCode == hashCode;
+
+  // 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 fgColor,
-    CellColor bgColor,
+    TerminalColor fgColor,
+    TerminalColor bgColor,
     bool bold,
     bool faint,
     bool italic,
@@ -86,15 +102,17 @@ class CellAttr {
 }
 
 class CellAttrTemplate {
-  CellAttrTemplate();
-  
+  CellAttrTemplate() {
+    reset();
+  }
+
   CellAttr _attr;
 
-  set fgColor(CellColor value) {
+  set fgColor(TerminalColor value) {
     _attr = _attr.copyWith(fgColor: value);
   }
 
-  set bgColor(CellColor value) {
+  set bgColor(TerminalColor value) {
     _attr = _attr.copyWith(bgColor: value);
   }
 
@@ -126,5 +144,13 @@ class CellAttrTemplate {
     _attr = _attr.copyWith(invisible: value);
   }
 
-  CellAttr get value {}
+  CellAttr get value => _attr;
+
+  void reset() {
+    _attr = CellAttr();
+  }
+
+  void use(CellAttr attr) {
+    _attr = attr;
+  }
 }

+ 0 - 24
lib/color/color_default.dart

@@ -1,24 +0,0 @@
-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),
-);

+ 0 - 48
lib/color/color_scheme.dart

@@ -1,48 +0,0 @@
-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;
-}

+ 15 - 15
lib/frontend/char_size.dart

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

+ 15 - 0
lib/frontend/input_behavior.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:xterm/xterm.dart';
+
+abstract class InputBehavior {
+  const InputBehavior();
+
+  bool get acceptKeyStroke;
+
+  void onKeyStroke(RawKeyEvent event, Terminal terminal);
+
+  TextEditingValue onTextEdit(TextEditingValue value, Terminal terminal);
+
+  void onAction(TextInputAction action, Terminal terminal);
+}

+ 42 - 0
lib/frontend/input_behavior_default.dart

@@ -0,0 +1,42 @@
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:xterm/frontend/input_behavior.dart';
+import 'package:xterm/frontend/input_map.dart';
+import 'package:xterm/xterm.dart';
+
+class InputBehaviorDefault extends InputBehavior {
+  const InputBehaviorDefault();
+
+  @override
+  bool get acceptKeyStroke => true;
+
+  @override
+  void onKeyStroke(RawKeyEvent event, Terminal terminal) {
+    if (event is! RawKeyDownEvent) {
+      return;
+    }
+
+    final key = inputMap(event.logicalKey);
+
+    if (key != null) {
+      terminal.keyInput(
+        key,
+        ctrl: event.isControlPressed,
+        alt: event.isAltPressed,
+        shift: event.isShiftPressed,
+      );
+    }
+  }
+
+  @override
+  TextEditingValue onTextEdit(TextEditingValue value, Terminal terminal) {
+    terminal.onInput(value.text);
+    print(value);
+    return TextEditingValue.empty;
+  }
+
+  @override
+  void onAction(TextInputAction action, Terminal terminal) {
+    //
+  }
+}

+ 5 - 0
lib/frontend/input_behavior_desktop.dart

@@ -0,0 +1,5 @@
+import 'package:xterm/frontend/input_behavior_default.dart';
+
+class InputBehaviorDesktop extends InputBehaviorDefault {
+  const InputBehaviorDesktop();
+}

+ 43 - 0
lib/frontend/input_behavior_mobile.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:xterm/frontend/input_behavior_default.dart';
+import 'package:xterm/input/keys.dart';
+import 'package:xterm/xterm.dart';
+
+class InputBehaviorMobile extends InputBehaviorDefault {
+  const InputBehaviorMobile();
+
+  static const _placeholder = '  ';
+
+  bool get acceptKeyStroke => false;
+
+  TextEditingValue onTextEdit(TextEditingValue value, Terminal terminal) {
+    if (value.text.length > _placeholder.length) {
+      terminal.onInput(value.text.substring(1, value.text.length - 1));
+    } else if (value.text.length < _placeholder.length) {
+      terminal.keyInput(TerminalKey.backspace);
+    } else {
+      if (value.selection.baseOffset < 1) {
+        terminal.keyInput(TerminalKey.arrowLeft);
+      } else if (value.selection.baseOffset > 1) {
+        terminal.keyInput(TerminalKey.arrowRight);
+      }
+    }
+
+    return TextEditingValue(
+      text: _placeholder,
+      selection: TextSelection.collapsed(offset: 1),
+    );
+  }
+
+  void onAction(TextInputAction action, Terminal terminal) {
+    print('action $action');
+    switch (action) {
+      case TextInputAction.done:
+        terminal.keyInput(TerminalKey.enter);
+        break;
+      default:
+        print('unknown action $action');
+    }
+  }
+}

+ 19 - 0
lib/frontend/input_behaviors.dart

@@ -0,0 +1,19 @@
+import 'dart:io';
+
+import 'package:xterm/frontend/input_behavior.dart';
+import 'package:xterm/frontend/input_behavior_desktop.dart';
+import 'package:xterm/frontend/input_behavior_mobile.dart';
+
+class InputBehaviors {
+  static const desktop = InputBehaviorDesktop();
+
+  static const mobile = InputBehaviorMobile();
+
+  static InputBehavior get platform {
+    if (Platform.isAndroid || Platform.isIOS) {
+      return mobile;
+    }
+
+    return desktop;
+  }
+}

+ 137 - 41
lib/frontend/input_listener.dart

@@ -1,106 +1,201 @@
+import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/scheduler.dart';
 import 'package:flutter/services.dart';
 import 'package:meta/meta.dart';
 
 typedef KeyStrokeHandler = void Function(RawKeyEvent);
-typedef InputHandler = void Function(String);
+typedef InputHandler = TextEditingValue Function(TextEditingValue);
+typedef ActionHandler = void Function(TextInputAction);
 typedef FocusHandler = void Function(bool);
 
+abstract class InputListenerController {
+  void requestKeyboard();
+}
+
 class InputListener extends StatefulWidget {
   InputListener({
     @required this.child,
     @required this.onKeyStroke,
-    @required this.onInput,
+    @required this.onTextInput,
+    @required this.onAction,
     @required this.focusNode,
     this.onFocus,
     this.autofocus = false,
+    this.listenKeyStroke = true,
+    this.readOnly = false,
   });
 
   final Widget child;
-  final InputHandler onInput;
+  final InputHandler onTextInput;
   final KeyStrokeHandler onKeyStroke;
+  final ActionHandler onAction;
   final FocusHandler onFocus;
   final bool autofocus;
   final FocusNode focusNode;
+  final bool listenKeyStroke;
+  final bool readOnly;
 
   @override
   InputListenerState createState() => InputListenerState();
+
+  static InputListenerController of(BuildContext context) {
+    return context.findAncestorStateOfType<InputListenerState>();
+  }
 }
 
-class InputListenerState extends State<InputListener> {
-  var focused = false;
-  TextInputConnection conn;
+class InputListenerState extends State<InputListener>
+    implements InputListenerController {
+  TextInputConnection _conn;
+  FocusAttachment _focusAttachment;
+  bool _didAutoFocus = false;
 
   @override
   void initState() {
-    focused = widget.focusNode.hasFocus;
-    widget.focusNode.addListener(onFocus);
+    _focusAttachment = widget.focusNode.attach(context);
+    widget.focusNode.addListener(onFocusChange);
     super.initState();
   }
 
   @override
-  void didUpdateWidget(InputListener oldWidget) {
-    oldWidget.focusNode.removeListener(onFocus);
-    widget.focusNode.addListener(onFocus);
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    if (!_didAutoFocus && widget.autofocus) {
+      _didAutoFocus = true;
+      SchedulerBinding.instance.addPostFrameCallback((_) {
+        if (mounted) {
+          FocusScope.of(context).autofocus(widget.focusNode);
+        }
+      });
+    }
+  }
+
+  bool get _shouldCreateInputConnection => kIsWeb || !widget.readOnly;
 
-    onFocus();
+  bool get _hasInputConnection => _conn != null && _conn.attached;
 
+  @override
+  void didUpdateWidget(InputListener oldWidget) {
     super.didUpdateWidget(oldWidget);
+
+    if (widget.focusNode != oldWidget.focusNode) {
+      oldWidget.focusNode.removeListener(onFocusChange);
+      _focusAttachment?.detach();
+      _focusAttachment = widget.focusNode.attach(context);
+      widget.focusNode.addListener(onFocusChange);
+    }
+
+    if (!_shouldCreateInputConnection) {
+      closeInputConnectionIfNeeded();
+    } else {
+      if (oldWidget.readOnly && widget.focusNode.hasFocus) {
+        openInputConnection();
+      }
+    }
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _focusAttachment.detach();
   }
 
   @override
   Widget build(BuildContext context) {
-    return RawKeyboardListener(
+    _focusAttachment.reparent();
+
+    if (widget.listenKeyStroke) {
+      return RawKeyboardListener(
+        focusNode: widget.focusNode,
+        onKey: widget.onKeyStroke,
+        autofocus: widget.autofocus,
+        child: widget.child,
+      );
+    }
+
+    return Focus(
       focusNode: widget.focusNode,
-      onKey: widget.onKeyStroke,
       autofocus: widget.autofocus,
+      includeSemantics: false,
       child: widget.child,
     );
   }
 
-  void onFocus() {
-    if (focused == widget.focusNode.hasFocus) {
-      return;
+  void requestKeyboard() {
+    if (widget.focusNode.hasFocus) {
+      openInputConnection();
+    } else {
+      widget.focusNode.requestFocus();
     }
+  }
 
-    focused = widget.focusNode.hasFocus;
-
+  void onFocusChange() {
     if (widget.onFocus != null) {
-      widget.onFocus(focused);
+      widget.onFocus(widget.focusNode.hasFocus);
     }
 
-    if (focused) {
-      openTextInput();
+    openOrCloseInputConnectionIfNeeded();
+  }
+
+  void openOrCloseInputConnectionIfNeeded() {
+    if (widget.focusNode.hasFocus && widget.focusNode.consumeKeyboardToken()) {
+      openInputConnection();
+    } else if (!widget.focusNode.hasFocus) {
+      closeInputConnectionIfNeeded();
     }
   }
 
-  void openTextInput() {
-    final config = TextInputConfiguration();
-    conn = TextInput.attach(
-      TerminalTextInputClient(onInput),
-      config,
-    );
+  void openInputConnection() {
+    if (!_shouldCreateInputConnection) {
+      return;
+    }
 
-    final dx = 0.0;
-    final dy = 0.0;
-    conn.setEditableSizeAndTransform(
-      Size(10, 10),
-      Matrix4.translationValues(dx, dy, 0.0),
-    );
+    if (_hasInputConnection) {
+      _conn.show();
+    } else {
+      final config = TextInputConfiguration();
+      final client = TerminalTextInputClient(onInput, onAction);
+      _conn = TextInput.attach(client, config);
+
+      _conn.show();
 
-    conn.show();
+      final dx = 0.0;
+      final dy = 0.0;
+      _conn.setEditableSizeAndTransform(
+        Size(10, 10),
+        Matrix4.translationValues(dx, dy, 0.0),
+      );
+
+      _conn.setEditingState(TextEditingValue.empty);
+    }
+  }
+
+  void closeInputConnectionIfNeeded() {
+    if (_conn != null && _conn.attached) {
+      _conn.close();
+      _conn = null;
+    }
+  }
+
+  void onInput(TextEditingValue value) {
+    final newValue = widget.onTextInput(value);
+
+    if (newValue != null) {
+      _conn?.setEditingState(newValue);
+    }
   }
 
-  void onInput(String text) {
-    widget.onInput(text);
-    conn?.setEditingState(TextEditingValue.empty);
+  void onAction(TextInputAction action) {
+    widget?.onAction(action);
   }
 }
 
 class TerminalTextInputClient extends TextInputClient {
-  TerminalTextInputClient(this.onInput);
+  TerminalTextInputClient(this.onInput, this.onAction);
 
-  final InputHandler onInput;
+  final void Function(TextEditingValue) onInput;
+  final ActionHandler onAction;
 
   TextEditingValue _savedValue;
 
@@ -115,7 +210,7 @@ class TerminalTextInputClient extends TextInputClient {
   void updateEditingValue(TextEditingValue value) {
     print('updateEditingValue $value');
 
-    onInput(value.text);
+    onInput(value);
 
     // if (_savedValue == null || _savedValue.text == '') {
     //   onInput(value.text);
@@ -130,6 +225,7 @@ class TerminalTextInputClient extends TextInputClient {
 
   void performAction(TextInputAction action) {
     // print('performAction $action');
+    onAction(action);
   }
 
   void updateFloatingCursor(RawFloatingCursorPoint point) {

+ 150 - 142
lib/frontend/terminal_view.dart

@@ -10,88 +10,70 @@ 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_behavior.dart';
+import 'package:xterm/frontend/input_behaviors.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/cache.dart';
 import 'package:xterm/mouse/position.dart';
 import 'package:xterm/terminal/terminal.dart';
+import 'package:xterm/theme/terminal_style.dart';
+import 'package:xterm/utli/hash_values.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,
+    this.style = const TerminalStyle(),
     FocusNode focusNode,
+    this.autofocus = false,
+    ScrollController scrollController,
+    InputBehavior inputBehavior,
   })  : assert(terminal != null),
         focusNode = focusNode ?? FocusNode(),
+        scrollController = scrollController ?? ScrollController(),
+        inputBehavior = inputBehavior ?? InputBehaviors.platform,
         super(key: key ?? ValueKey(terminal));
 
   final Terminal terminal;
   final ResizeHandler onResize;
   final FocusNode focusNode;
+  final bool autofocus;
+  final ScrollController scrollController;
+
+  final TerminalStyle style;
 
-  final double fontSize;
-  final double fontWidthScaleFactor;
-  final double fontHeightScaleFactor;
-  final List<String> fontFamily;
+  final InputBehavior inputBehavior;
 
-  CharSize getCharSize() {
+  CellSize measureCellSize() {
     final testString = 'xxxxxxxxxx' * 1000;
+
     final text = Text(
       testString,
       style: TextStyle(
-        fontFamilyFallback: _kDefaultFontFamily,
-        fontSize: fontSize,
+        fontFamilyFallback: style.fontFamily,
+        fontSize: style.fontSize,
       ),
     );
-    final size = textSize(text);
 
-    final width = (size.width / testString.length);
-    final height = size.height;
+    final size = textSize(text);
 
-    final effectWidth = width * fontWidthScaleFactor;
-    final effectHeight = size.height * fontHeightScaleFactor;
+    final charWidth = (size.width / testString.length);
+    final charHeight = size.height;
 
-    // final ls
+    final cellWidth = charWidth * style.fontWidthScaleFactor;
+    final cellHeight = size.height * style.fontHeightScaleFactor;
 
-    return CharSize(
-      width: width,
-      height: height,
-      effectWidth: effectWidth,
-      effectHeight: effectHeight,
-      letterSpacing: effectWidth - width,
-      lineSpacing: effectHeight - height,
+    return CellSize(
+      charWidth: charWidth,
+      charHeight: charHeight,
+      cellWidth: cellWidth,
+      cellHeight: cellHeight,
+      letterSpacing: cellWidth - charWidth,
+      lineSpacing: cellHeight - charHeight,
     );
   }
 
@@ -108,9 +90,22 @@ class _TerminalViewState extends State<TerminalView> {
 
   int _lastTerminalWidth;
   int _lastTerminalHeight;
-  CharSize _charSize;
+  CellSize _cellSize;
+  ViewportOffset _offset;
+
+  var _minScrollExtent = 0.0;
+  var _maxScrollExtent = 0.0;
 
   void onTerminalChange() {
+    // if (_offset != null) {
+    //   final currentScrollExtent =
+    //       _cellSize.cellHeight * widget.terminal.buffer.scrollOffsetFromTop;
+
+    //   if (_offset.pixels != currentScrollExtent) {
+    //     _offset.correctBy(currentScrollExtent - _offset.pixels - 1);
+    //   }
+    // }
+
     if (mounted) {
       setState(() {});
     }
@@ -124,7 +119,7 @@ class _TerminalViewState extends State<TerminalView> {
   void initState() {
     // oscillator.start();
     // oscillator.addListener(onTick);
-    _charSize = widget.getCharSize();
+    _cellSize = widget.measureCellSize();
     widget.terminal.addListener(onTerminalChange);
     super.initState();
   }
@@ -147,26 +142,57 @@ class _TerminalViewState extends State<TerminalView> {
 
   @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,
-        ),
+    return InputListener(
+      listenKeyStroke: widget.inputBehavior.acceptKeyStroke,
+      onKeyStroke: onKeyStroke,
+      onTextInput: onInput,
+      onAction: onAction,
+      onFocus: onFocus,
+      focusNode: widget.focusNode,
+      autofocus: false,
+      child: MouseRegion(
+        cursor: SystemMouseCursors.text,
+        child: LayoutBuilder(builder: (context, constraints) {
+          onResize(constraints.maxWidth, constraints.maxHeight);
+          return Scrollable(
+            viewportBuilder: (context, offset) {
+              offset.applyViewportDimension(constraints.maxHeight);
+
+              _minScrollExtent = 0.0;
+
+              _maxScrollExtent = math.max(
+                  0.0,
+                  _cellSize.cellHeight * widget.terminal.buffer.height -
+                      constraints.maxHeight);
+
+              // final currentScrollExtent = _cellSize.cellHeight *
+              //     widget.terminal.buffer.scrollOffsetFromTop;
+
+              // offset.correctBy(currentScrollExtent - offset.pixels - 1);
+
+              offset.applyContentDimensions(_minScrollExtent, _maxScrollExtent);
+
+              _offset = offset;
+              _offset.addListener(onScroll);
+
+              return buildTerminal(context);
+            },
+          );
+        }),
       ),
     );
+  }
 
-    result = GestureDetector(
-      child: result,
+  Widget buildTerminal(BuildContext context) {
+    return GestureDetector(
       behavior: HitTestBehavior.deferToChild,
       dragStartBehavior: DragStartBehavior.down,
       onTapDown: (detail) {
-        widget.focusNode.requestFocus();
+        if (widget.terminal.selection.isEmpty) {
+          InputListener.of(context).requestKeyboard();
+        } else {
+          widget.terminal.selection.clear();
+        }
         final pos = detail.localPosition;
         final offset = getMouseOffset(pos.dx, pos.dy);
         widget.terminal.mouseMode.onTap(widget.terminal, offset);
@@ -184,41 +210,36 @@ class _TerminalViewState extends State<TerminalView> {
         widget.terminal.mouseMode.onPanUpdate(widget.terminal, offset);
         widget.terminal.refresh();
       },
-    );
-
-    return InputListener(
-      onKeyStroke: onKeyStroke,
-      onInput: onInput,
-      onFocus: onFocus,
-      focusNode: widget.focusNode,
-      autofocus: true,
-      child: MouseRegion(
-        cursor: SystemMouseCursors.text,
-        child: MouseListener(
-          onScroll: onScroll,
-          child: LayoutBuilder(builder: (context, constraints) {
-            onResize(constraints.maxWidth, constraints.maxHeight);
-            return result;
-          }),
+      child: Container(
+        constraints: BoxConstraints.expand(),
+        color: Color(widget.terminal.theme.background.value),
+        child: CustomPaint(
+          painter: TerminalPainter(
+            terminal: widget.terminal,
+            view: widget,
+            oscillator: oscillator,
+            focused: focused,
+            charSize: _cellSize,
+          ),
         ),
       ),
     );
   }
 
   Position getMouseOffset(double px, double py) {
-    final col = (px / _charSize.effectWidth).floor();
-    final row = (py / _charSize.effectHeight).floor();
+    final col = (px / _cellSize.cellWidth).floor();
+    final row = (py / _cellSize.cellHeight).floor();
 
     final x = col;
     final y = widget.terminal.buffer.convertViewLineToRawLine(row) -
-        widget.terminal.buffer.scrollOffset;
+        widget.terminal.buffer.scrollOffsetFromBottom;
 
     return Position(x, y);
   }
 
   void onResize(double width, double height) {
-    final termWidth = (width / _charSize.effectWidth).floor();
-    final termHeight = (height / _charSize.effectHeight).floor();
+    final termWidth = (width / _cellSize.cellWidth).floor();
+    final termHeight = (height / _cellSize.cellHeight).floor();
 
     if (_lastTerminalWidth != termWidth || _lastTerminalHeight != termHeight) {
       _lastTerminalWidth = termWidth;
@@ -240,27 +261,13 @@ class _TerminalViewState extends State<TerminalView> {
     }
   }
 
-  void onInput(String input) {
-    widget.terminal.onInput(input);
+  TextEditingValue onInput(TextEditingValue value) {
+    return widget.inputBehavior.onTextEdit(value, widget.terminal);
   }
 
   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);
+    widget.inputBehavior.onKeyStroke(event, widget.terminal);
+    _offset.moveTo(_maxScrollExtent);
   }
 
   void onFocus(bool focused) {
@@ -269,14 +276,14 @@ class _TerminalViewState extends State<TerminalView> {
     });
   }
 
-  void onScroll(Offset offset) {
-    final delta = math.max(1, offset.dy.abs() ~/ 10);
+  void onAction(TextInputAction action) {
+    widget.inputBehavior.onAction(action, widget.terminal);
+  }
 
-    if (offset.dy > 0) {
-      widget.terminal.buffer.screenScrollDown(delta);
-    } else if (offset.dy < 0) {
-      widget.terminal.buffer.screenScrollUp(delta);
-    }
+  void onScroll() {
+    final charOffset = (_offset.pixels / _cellSize.cellHeight).ceil();
+    final offset = widget.terminal.invisibleHeight - charOffset;
+    widget.terminal.buffer.setScrollOffsetFromBottom(offset);
   }
 }
 
@@ -293,7 +300,7 @@ class TerminalPainter extends CustomPainter {
   final TerminalView view;
   final Oscillator oscillator;
   final bool focused;
-  final CharSize charSize;
+  final CellSize charSize;
 
   @override
   void paint(Canvas canvas, Size size) {
@@ -318,7 +325,7 @@ class TerminalPainter extends CustomPainter {
 
     for (var i = 0; i < lines.length; i++) {
       final line = lines[i];
-      final offsetY = i * charSize.effectHeight;
+      final offsetY = i * charSize.cellHeight;
       final cellCount = math.min(terminal.viewWidth, line.length);
 
       for (var i = 0; i < cellCount; i++) {
@@ -328,9 +335,9 @@ class TerminalPainter extends CustomPainter {
           continue;
         }
 
-        final offsetX = i * charSize.effectWidth;
-        final effectWidth = charSize.effectWidth * cell.width + 1;
-        final effectHeight = charSize.effectHeight + 1;
+        final offsetX = i * charSize.cellWidth;
+        final effectWidth = charSize.cellWidth * cell.width + 1;
+        final effectHeight = charSize.cellHeight + 1;
 
         final bgColor =
             cell.attr.inverse ? cell.attr.fgColor : cell.attr.bgColor;
@@ -350,9 +357,9 @@ class TerminalPainter extends CustomPainter {
 
   void paintSelection(Canvas canvas) {
     for (var y = 0; y < terminal.viewHeight; y++) {
-      final offsetY = y * charSize.effectHeight;
+      final offsetY = y * charSize.cellHeight;
       final absoluteY = terminal.buffer.convertViewLineToRawLine(y) -
-          terminal.buffer.scrollOffset;
+          terminal.buffer.scrollOffsetFromBottom;
 
       for (var x = 0; x < terminal.viewWidth; x++) {
         var cellCount = 0;
@@ -367,9 +374,9 @@ class TerminalPainter extends CustomPainter {
           continue;
         }
 
-        final offsetX = x * charSize.effectWidth;
-        final effectWidth = cellCount * charSize.effectWidth;
-        final effectHeight = charSize.effectHeight;
+        final offsetX = x * charSize.cellWidth;
+        final effectWidth = cellCount * charSize.cellWidth;
+        final effectHeight = charSize.cellHeight;
 
         final paint = Paint()..color = Colors.white.withOpacity(0.3);
         canvas.drawRect(
@@ -387,7 +394,7 @@ class TerminalPainter extends CustomPainter {
 
     for (var i = 0; i < lines.length; i++) {
       final line = lines[i];
-      final offsetY = i * charSize.effectHeight;
+      final offsetY = i * charSize.cellHeight;
       final cellCount = math.min(terminal.viewWidth, line.length);
 
       for (var i = 0; i < cellCount; i++) {
@@ -397,7 +404,7 @@ class TerminalPainter extends CustomPainter {
           continue;
         }
 
-        final offsetX = i * charSize.effectWidth;
+        final offsetX = i * charSize.cellWidth;
         paintCell(canvas, cell, offsetX, offsetY);
       }
     }
@@ -408,7 +415,7 @@ class TerminalPainter extends CustomPainter {
 
     for (var i = 0; i < lines.length; i++) {
       final line = lines[i];
-      final offsetY = i * charSize.effectHeight;
+      final offsetY = i * charSize.cellHeight;
       final cellCount = math.min(terminal.viewWidth, line.length);
 
       final builder = StringBuffer();
@@ -433,12 +440,12 @@ class TerminalPainter extends CustomPainter {
         // color: color,
         // fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
         // fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
-        fontSize: view.fontSize,
+        fontSize: view.style.fontSize,
         letterSpacing: charSize.letterSpacing,
         fontFeatures: [FontFeature.tabularFigures()],
         // decoration:
         //     cell.attr.underline ? TextDecoration.underline : TextDecoration.none,
-        fontFamilyFallback: _kDefaultFontFamily,
+        fontFamilyFallback: view.style.fontFamily,
       );
 
       final span = TextSpan(
@@ -458,8 +465,8 @@ class TerminalPainter extends CustomPainter {
     }
 
     final cellColor = cell.attr.inverse
-        ? cell.attr.bgColor ?? terminal.colorScheme.background
-        : cell.attr.fgColor;
+        ? cell.attr.bgColor ?? terminal.theme.background
+        : cell.attr.fgColor ?? terminal.theme.foreground;
 
     var color = Color(cellColor.value);
 
@@ -468,14 +475,14 @@ class TerminalPainter extends CustomPainter {
     }
 
     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,
-    );
+        color: color,
+        fontWeight: cell.attr.bold ? FontWeight.bold : FontWeight.normal,
+        fontStyle: cell.attr.italic ? FontStyle.italic : FontStyle.normal,
+        fontSize: view.style.fontSize,
+        decoration: cell.attr.underline
+            ? TextDecoration.underline
+            : TextDecoration.none,
+        fontFamilyFallback: view.style.fontFamily);
 
     final span = TextSpan(
       text: String.fromCharCode(cell.codePoint),
@@ -484,7 +491,8 @@ class TerminalPainter extends CustomPainter {
     );
 
     // final tp = textLayoutCache.getOrPerformLayout(span);
-    final tp = textLayoutCacheV2.getOrPerformLayout(span, cell.codePoint);
+    final tp = textLayoutCacheV2.getOrPerformLayout(
+        span, hashValues(cell.codePoint, cell.attr));
 
     tp.paint(canvas, Offset(offsetX, offsetY));
   }
@@ -497,16 +505,16 @@ class TerminalPainter extends CustomPainter {
 
     final char = terminal.buffer.getCellUnderCursor();
     final width =
-        char != null ? charSize.effectWidth * char.width : charSize.effectWidth;
+        char != null ? charSize.cellWidth * char.width : charSize.cellWidth;
 
-    final offsetX = charSize.effectWidth * terminal.cursorX;
-    final offsetY = charSize.effectHeight * screenCursorY;
+    final offsetX = charSize.cellWidth * terminal.cursorX;
+    final offsetY = charSize.cellHeight * screenCursorY;
     final paint = Paint()
-      ..color = Color(terminal.colorScheme.cursor.value)
+      ..color = Color(terminal.theme.cursor.value)
       ..strokeWidth = focused ? 0.0 : 1.0
       ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
     canvas.drawRect(
-        Rect.fromLTWH(offsetX, offsetY, width, charSize.effectHeight), paint);
+        Rect.fromLTWH(offsetX, offsetY, width, charSize.cellHeight), paint);
   }
 
   @override

+ 1 - 1
lib/input/shortcut.dart

@@ -1 +1 @@
-// TBD
+// TBD

+ 0 - 1
lib/mouse/mouse_mode.dart

@@ -20,7 +20,6 @@ class MouseModeNone extends MouseMode {
   @override
   void onTap(Terminal terminal, Position offset) {
     terminal.debug.onMsg('tap: $offset');
-    terminal.selection.clear();
   }
 
   @override

+ 2 - 1
lib/terminal/csi.dart

@@ -223,7 +223,8 @@ void csiDeviceStatusReportHandler(CSI csi, Terminal terminal) {
       terminal.onInput("\x1b[${terminal.cursorX + 1};${terminal.cursorY + 1}R");
       break;
     default:
-      terminal.debug.onError('Unknown Device Status Report identifier: ${csi.params[0]}');
+      terminal.debug
+          .onError('Unknown Device Status Report identifier: ${csi.params[0]}');
       return;
   }
 }

+ 6 - 6
lib/terminal/modes.dart

@@ -84,12 +84,12 @@ void csiSetMode(String mode, bool enabled, Terminal terminal) {
       terminal.setAutoWrapMode(enabled);
       break;
     case "?9":
-    	if (enabled) {
-    		// terminal.setMouseMode(MouseMode.x10);
-    	} else {
-    		terminal.setMouseMode(MouseMode.none);
-    	}
-     break;
+      if (enabled) {
+        // terminal.setMouseMode(MouseMode.x10);
+      } else {
+        terminal.setMouseMode(MouseMode.none);
+      }
+      break;
     case "?12":
     case "?13":
       terminal.setBlinkingCursor(enabled);

+ 73 - 63
lib/terminal/sgr.dart

@@ -1,6 +1,16 @@
-import 'package:xterm/buffer/cell_color.dart';
+import 'package:xterm/theme/terminal_color.dart';
 import 'package:xterm/terminal/csi.dart';
 import 'package:xterm/terminal/terminal.dart';
+import 'package:xterm/theme/terminal_color_ref.dart';
+
+// reference to color
+class Cr implements TerminalColor {
+  Cr(this.getter);
+
+  final TerminalColor Function() getter;
+
+  int get value => getter().value;
+}
 
 /// 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.
@@ -20,7 +30,7 @@ void sgrHandler(CSI csi, Terminal terminal) {
       case '':
       case '0':
       case '00':
-        terminal.cellAttr.reset(fgColor: terminal.colorScheme.foreground);
+        terminal.cellAttr.reset();
         break;
       case '1':
       case '01':
@@ -75,106 +85,106 @@ void sgrHandler(CSI csi, Terminal terminal) {
         // not strikethrough
         break;
       case '39':
-        terminal.cellAttr.fgColor = terminal.colorScheme.foreground;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.foreground);
         break;
       case '30':
-        terminal.cellAttr.fgColor = terminal.colorScheme.black;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.black);
         break;
       case '31':
-        terminal.cellAttr.fgColor = terminal.colorScheme.red;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.red);
         break;
       case '32':
-        terminal.cellAttr.fgColor = terminal.colorScheme.green;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.green);
         break;
       case '33':
-        terminal.cellAttr.fgColor = terminal.colorScheme.yellow;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.yellow);
         break;
       case '34':
-        terminal.cellAttr.fgColor = terminal.colorScheme.blue;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.blue);
         break;
       case '35':
-        terminal.cellAttr.fgColor = terminal.colorScheme.magenta;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.magenta);
         break;
       case '36':
-        terminal.cellAttr.fgColor = terminal.colorScheme.cyan;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.cyan);
         break;
       case '37':
-        terminal.cellAttr.fgColor = terminal.colorScheme.white;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.white);
         break;
       case '90':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightBlack;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightBlack);
         break;
       case '91':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightRed;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightRed);
         break;
       case '92':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightGreen;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightGreen);
         break;
       case '93':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightYellow;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightYellow);
         break;
       case '94':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightBlue;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightBlue);
         break;
       case '95':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightMagenta;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightMagenta);
         break;
       case '96':
-        terminal.cellAttr.fgColor = terminal.colorScheme.brightCyan;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.brightCyan);
         break;
       case '97':
-        terminal.cellAttr.fgColor = terminal.colorScheme.white;
+        terminal.cellAttr.fgColor = Cr(() => terminal.theme.white);
         break;
       case '49':
-        terminal.cellAttr.bgColor = terminal.colorScheme.background;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.background);
         break;
       case '40':
-        terminal.cellAttr.bgColor = terminal.colorScheme.black;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.black);
         break;
       case '41':
-        terminal.cellAttr.bgColor = terminal.colorScheme.red;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.red);
         break;
       case '42':
-        terminal.cellAttr.bgColor = terminal.colorScheme.green;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.green);
         break;
       case '43':
-        terminal.cellAttr.bgColor = terminal.colorScheme.yellow;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.yellow);
         break;
       case '44':
-        terminal.cellAttr.bgColor = terminal.colorScheme.blue;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.blue);
         break;
       case '45':
-        terminal.cellAttr.bgColor = terminal.colorScheme.magenta;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.magenta);
         break;
       case '46':
-        terminal.cellAttr.bgColor = terminal.colorScheme.cyan;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.cyan);
         break;
       case '47':
-        terminal.cellAttr.bgColor = terminal.colorScheme.white;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.white);
         break;
       case '100':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightBlack;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightBlack);
         break;
       case '101':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightRed;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightRed);
         break;
       case '102':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightGreen;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightGreen);
         break;
       case '103':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightYellow;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightYellow);
         break;
       case '104':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightBlue;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightBlue);
         break;
       case '105':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightMagenta;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightMagenta);
         break;
       case '106':
-        terminal.cellAttr.bgColor = terminal.colorScheme.brightCyan;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.brightCyan);
         break;
       case '107':
-        terminal.cellAttr.bgColor = terminal.colorScheme.white;
+        terminal.cellAttr.bgColor = Cr(() => terminal.theme.white);
         break;
       case '38': // set foreground
         final color = parseAnsiColour(params.sublist(i), terminal);
@@ -190,7 +200,7 @@ void sgrHandler(CSI csi, Terminal terminal) {
   }
 }
 
-CellColor parseAnsiColour(List<String> params, Terminal terminal) {
+TerminalColor parseAnsiColour(List<String> params, Terminal terminal) {
   if (params.length > 2) {
     switch (params[1]) {
       case "5":
@@ -198,14 +208,14 @@ CellColor parseAnsiColour(List<String> params, Terminal terminal) {
         final colNum = int.tryParse(params[2]);
 
         if (colNum == null || colNum >= 256 || colNum < 0) {
-          return CellColor.empty();
+          return TerminalColor.empty();
         }
 
         return parse8BitSgrColour(colNum, terminal);
 
       case "2":
         if (params.length < 4) {
-          return CellColor.empty();
+          return TerminalColor.empty();
         }
 
         // 24 bit colour
@@ -215,10 +225,10 @@ CellColor parseAnsiColour(List<String> params, Terminal terminal) {
           final b = int.tryParse(params[4]);
 
           if (r == null || g == null || b == null) {
-            return CellColor.empty();
+            return TerminalColor.empty();
           }
 
-          return CellColor.fromARGB(0xff, r, g, b);
+          return TerminalColor.fromARGB(0xff, r, g, b);
         }
 
         if (params.length > 5) {
@@ -228,15 +238,15 @@ CellColor parseAnsiColour(List<String> params, Terminal terminal) {
           final b = int.tryParse(params[5]);
 
           if (r == null || g == null || b == null) {
-            return CellColor.empty();
+            return TerminalColor.empty();
           }
 
-          return CellColor.fromARGB(0xff, r, g, b);
+          return TerminalColor.fromARGB(0xff, r, g, b);
         }
     }
   }
 
-  return CellColor.empty();
+  return TerminalColor.empty();
 }
 
 const grayscaleColors = {
@@ -266,42 +276,42 @@ const grayscaleColors = {
   255: 0xffeeeeee,
 };
 
-CellColor parse8BitSgrColour(int colNum, Terminal terminal) {
+TerminalColor parse8BitSgrColour(int colNum, Terminal terminal) {
   // https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
 
   switch (colNum) {
     case 0:
-      return terminal.colorScheme.black;
+      return terminal.theme.black;
     case 1:
-      return terminal.colorScheme.red;
+      return terminal.theme.red;
     case 2:
-      return terminal.colorScheme.green;
+      return terminal.theme.green;
     case 3:
-      return terminal.colorScheme.yellow;
+      return terminal.theme.yellow;
     case 4:
-      return terminal.colorScheme.blue;
+      return terminal.theme.blue;
     case 5:
-      return terminal.colorScheme.magenta;
+      return terminal.theme.magenta;
     case 6:
-      return terminal.colorScheme.cyan;
+      return terminal.theme.cyan;
     case 7:
-      return terminal.colorScheme.white;
+      return terminal.theme.white;
     case 8:
-      return terminal.colorScheme.brightBlack;
+      return terminal.theme.brightBlack;
     case 9:
-      return terminal.colorScheme.brightRed;
+      return terminal.theme.brightRed;
     case 10:
-      return terminal.colorScheme.brightGreen;
+      return terminal.theme.brightGreen;
     case 11:
-      return terminal.colorScheme.brightYellow;
+      return terminal.theme.brightYellow;
     case 12:
-      return terminal.colorScheme.brightBlue;
+      return terminal.theme.brightBlue;
     case 13:
-      return terminal.colorScheme.brightMagenta;
+      return terminal.theme.brightMagenta;
     case 14:
-      return terminal.colorScheme.brightCyan;
+      return terminal.theme.brightCyan;
     case 15:
-      return terminal.colorScheme.white;
+      return terminal.theme.white;
   }
 
   if (colNum < 232) {
@@ -335,8 +345,8 @@ CellColor parse8BitSgrColour(int colNum, Terminal terminal) {
       }
     }
 
-    return CellColor.fromARGB(0xff, r, g, b);
+    return TerminalColor.fromARGB(0xff, r, g, b);
   }
 
-  return CellColor(grayscaleColors[colNum.clamp(232, 255)]);
+  return TerminalColor(grayscaleColors[colNum.clamp(232, 255)]);
 }

+ 16 - 9
lib/terminal/terminal.dart

@@ -1,12 +1,11 @@
 import 'dart:async';
-import 'dart:math' show max;
+import 'dart:math' show max, min;
 
 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';
@@ -16,6 +15,8 @@ import 'package:xterm/terminal/ansi.dart';
 import 'package:xterm/terminal/platform.dart';
 import 'package:xterm/terminal/sbc.dart';
 import 'package:xterm/terminal/tabs.dart';
+import 'package:xterm/theme/terminal_theme.dart';
+import 'package:xterm/theme/terminal_themes.dart';
 import 'package:xterm/utli/debug_handler.dart';
 import 'package:xterm/utli/observable.dart';
 
@@ -31,6 +32,7 @@ class Terminal with Observable {
     this.onTitleChange,
     this.onIconChange,
     this.platform = PlatformBehavior.unix,
+    this.theme = TerminalThemes.defaultTheme,
     int maxLines,
   }) {
     _maxLines = maxLines;
@@ -70,6 +72,9 @@ class Terminal with Observable {
   int get viewWidth => _viewWidth;
   int get viewHeight => _viewHeight;
 
+  int get visibleHeight => min(_viewHeight, buffer.height);
+  int get invisibleHeight => buffer.height - visibleHeight;
+
   bool _originMode = false;
   bool _replaceMode = false;
   bool _lineFeed = true;
@@ -103,8 +108,8 @@ class Terminal with Observable {
   MouseMode _mouseMode = MouseMode.none;
   MouseMode get mouseMode => _mouseMode;
 
-  final colorScheme = defaultColorScheme;
-  var cellAttr = CellAttr(fgColor: defaultColorScheme.foreground);
+  final TerminalTheme theme;
+  final cellAttr = CellAttrTemplate();
 
   final keytab = Keytab.defaultKeytab();
   final selection = Selection();
@@ -128,7 +133,7 @@ class Terminal with Observable {
 
   int get cursorX => buffer.cursorX;
   int get cursorY => buffer.cursorY;
-  int get scrollOffset => buffer.scrollOffset;
+  int get scrollOffset => buffer.scrollOffsetFromBottom;
 
   void write(String text) async {
     for (var char in text.runes) {
@@ -266,13 +271,15 @@ class Terminal with Observable {
     }
   }
 
-  void input(
+  void keyInput(
     TerminalKey key, {
-    bool ctrl,
-    bool alt,
-    bool shift,
+    bool ctrl = false,
+    bool alt = false,
+    bool shift = false,
     // bool meta,
   }) {
+    debug.onMsg(key);
+
     if (onInput == null) {
       return;
     }

+ 6 - 4
lib/buffer/cell_color.dart → lib/theme/terminal_color.dart

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

+ 9 - 0
lib/theme/terminal_color_ref.dart

@@ -0,0 +1,9 @@
+import 'package:xterm/theme/terminal_color.dart';
+
+class TerminalColorRef implements TerminalColor {
+  TerminalColorRef(this.getter);
+
+  final TerminalColor Function() getter;
+
+  int get value => getter().value;
+}

+ 36 - 0
lib/theme/terminal_style.dart

@@ -0,0 +1,36 @@
+class TerminalStyle {
+  static const defaultFontFamily = [
+    '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',
+  ];
+
+  const TerminalStyle({
+    this.fontFamily = defaultFontFamily,
+    this.fontSize = 14,
+    this.fontWidthScaleFactor = 1.0,
+    this.fontHeightScaleFactor = 1.1,
+  });
+
+  final List<String> fontFamily;
+  final double fontSize;
+  final double fontWidthScaleFactor;
+  final double fontHeightScaleFactor;
+}

+ 48 - 0
lib/theme/terminal_theme.dart

@@ -0,0 +1,48 @@
+import 'package:meta/meta.dart';
+import 'package:xterm/theme/terminal_color.dart';
+
+class TerminalTheme {
+  const TerminalTheme({
+    @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,
+  });
+
+  final TerminalColor cursor;
+  final TerminalColor selection;
+
+  final TerminalColor foreground;
+  final TerminalColor background;
+  final TerminalColor black;
+  final TerminalColor red;
+  final TerminalColor green;
+  final TerminalColor yellow;
+  final TerminalColor blue;
+  final TerminalColor magenta;
+  final TerminalColor cyan;
+
+  final TerminalColor brightBlack;
+  final TerminalColor brightRed;
+  final TerminalColor brightGreen;
+  final TerminalColor brightYellow;
+  final TerminalColor brightBlue;
+  final TerminalColor brightMagenta;
+  final TerminalColor brightCyan;
+  final TerminalColor white;
+}

+ 48 - 0
lib/theme/terminal_themes.dart

@@ -0,0 +1,48 @@
+import 'package:xterm/theme/terminal_color.dart';
+import 'package:xterm/theme/terminal_theme.dart';
+
+class TerminalThemes {
+  static const defaultTheme = TerminalTheme(
+    cursor: TerminalColor(0xffaeafad),
+    selection: TerminalColor(0xffffff40),
+    foreground: TerminalColor(0xffcccccc),
+    background: TerminalColor(0xff1e1e1e),
+    black: TerminalColor(0xff000000),
+    white: TerminalColor(0xffe5e5e5),
+    red: TerminalColor(0xffcd3131),
+    green: TerminalColor(0xff0dbc79),
+    yellow: TerminalColor(0xffe5e510),
+    blue: TerminalColor(0xff2472c8),
+    magenta: TerminalColor(0xffbc3fbc),
+    cyan: TerminalColor(0xff11a8cd),
+    brightBlack: TerminalColor(0xff666666),
+    brightRed: TerminalColor(0xfff14c4c),
+    brightGreen: TerminalColor(0xff23d18b),
+    brightYellow: TerminalColor(0xfff5f543),
+    brightBlue: TerminalColor(0xff3b8eea),
+    brightMagenta: TerminalColor(0xffd670d6),
+    brightCyan: TerminalColor(0xff29b8db),
+  );
+
+  static const whiteOnBlack = TerminalTheme(
+    cursor: TerminalColor(0xffaeafad),
+    selection: TerminalColor(0xffffff40),
+    foreground: TerminalColor(0xffffffff),
+    background: TerminalColor(0xff000000),
+    black: TerminalColor(0xff000000),
+    white: TerminalColor(0xffe5e5e5),
+    red: TerminalColor(0xffcd3131),
+    green: TerminalColor(0xff0dbc79),
+    yellow: TerminalColor(0xffe5e510),
+    blue: TerminalColor(0xff2472c8),
+    magenta: TerminalColor(0xffbc3fbc),
+    cyan: TerminalColor(0xff11a8cd),
+    brightBlack: TerminalColor(0xff666666),
+    brightRed: TerminalColor(0xfff14c4c),
+    brightGreen: TerminalColor(0xff23d18b),
+    brightYellow: TerminalColor(0xfff5f543),
+    brightBlue: TerminalColor(0xff3b8eea),
+    brightMagenta: TerminalColor(0xffd670d6),
+    brightCyan: TerminalColor(0xff29b8db),
+  );
+}

+ 109 - 0
lib/utli/hash_values.dart

@@ -0,0 +1,109 @@
+class _HashEnd {
+  const _HashEnd();
+}
+
+const _HashEnd _hashEnd = _HashEnd();
+
+class _Jenkins {
+  static int combine(int hash, Object o) {
+    assert(o is! Iterable);
+    hash = 0x1fffffff & (hash + o.hashCode);
+    hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+    return hash ^ (hash >> 6);
+  }
+
+  static int finish(int hash) {
+    hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+    hash = hash ^ (hash >> 11);
+    return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+  }
+}
+
+int hashValues(Object arg01, Object arg02,
+    [Object arg03 = _hashEnd,
+    Object arg04 = _hashEnd,
+    Object arg05 = _hashEnd,
+    Object arg06 = _hashEnd,
+    Object arg07 = _hashEnd,
+    Object arg08 = _hashEnd,
+    Object arg09 = _hashEnd,
+    Object arg10 = _hashEnd,
+    Object arg11 = _hashEnd,
+    Object arg12 = _hashEnd,
+    Object arg13 = _hashEnd,
+    Object arg14 = _hashEnd,
+    Object arg15 = _hashEnd,
+    Object arg16 = _hashEnd,
+    Object arg17 = _hashEnd,
+    Object arg18 = _hashEnd,
+    Object arg19 = _hashEnd,
+    Object arg20 = _hashEnd]) {
+  int result = 0;
+  result = _Jenkins.combine(result, arg01);
+  result = _Jenkins.combine(result, arg02);
+  if (!identical(arg03, _hashEnd)) {
+    result = _Jenkins.combine(result, arg03);
+    if (!identical(arg04, _hashEnd)) {
+      result = _Jenkins.combine(result, arg04);
+      if (!identical(arg05, _hashEnd)) {
+        result = _Jenkins.combine(result, arg05);
+        if (!identical(arg06, _hashEnd)) {
+          result = _Jenkins.combine(result, arg06);
+          if (!identical(arg07, _hashEnd)) {
+            result = _Jenkins.combine(result, arg07);
+            if (!identical(arg08, _hashEnd)) {
+              result = _Jenkins.combine(result, arg08);
+              if (!identical(arg09, _hashEnd)) {
+                result = _Jenkins.combine(result, arg09);
+                if (!identical(arg10, _hashEnd)) {
+                  result = _Jenkins.combine(result, arg10);
+                  if (!identical(arg11, _hashEnd)) {
+                    result = _Jenkins.combine(result, arg11);
+                    if (!identical(arg12, _hashEnd)) {
+                      result = _Jenkins.combine(result, arg12);
+                      if (!identical(arg13, _hashEnd)) {
+                        result = _Jenkins.combine(result, arg13);
+                        if (!identical(arg14, _hashEnd)) {
+                          result = _Jenkins.combine(result, arg14);
+                          if (!identical(arg15, _hashEnd)) {
+                            result = _Jenkins.combine(result, arg15);
+                            if (!identical(arg16, _hashEnd)) {
+                              result = _Jenkins.combine(result, arg16);
+                              if (!identical(arg17, _hashEnd)) {
+                                result = _Jenkins.combine(result, arg17);
+                                if (!identical(arg18, _hashEnd)) {
+                                  result = _Jenkins.combine(result, arg18);
+                                  if (!identical(arg19, _hashEnd)) {
+                                    result = _Jenkins.combine(result, arg19);
+                                    if (!identical(arg20, _hashEnd)) {
+                                      result = _Jenkins.combine(result, arg20);
+                                      // I can see my house from here!
+                                    }
+                                  }
+                                }
+                              }
+                            }
+                          }
+                        }
+                      }
+                    }
+                  }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+  return _Jenkins.finish(result);
+}
+
+int hashList(Iterable<Object> arguments) {
+  int result = 0;
+  if (arguments != null) {
+    for (Object argument in arguments)
+      result = _Jenkins.combine(result, argument);
+  }
+  return _Jenkins.finish(result);
+}

+ 1 - 1
lib/xterm.dart

@@ -1,4 +1,4 @@
 library xterm;
 
 export 'terminal/terminal.dart';
-export 'terminal/platform.dart';
+export 'terminal/platform.dart';

+ 20 - 20
pubspec.lock

@@ -5,56 +5,56 @@ packages:
     dependency: "direct main"
     description:
       name: async
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.5.0-nullsafety"
   boolean_selector:
     dependency: transitive
     description:
       name: boolean_selector
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety"
   characters:
     dependency: transitive
     description:
       name: characters
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety.2"
   charcode:
     dependency: transitive
     description:
       name: charcode
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.2.0-nullsafety"
   clock:
     dependency: transitive
     description:
       name: clock
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   collection:
     dependency: transitive
     description:
       name: collection
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.15.0-nullsafety.2"
   convert:
     dependency: "direct main"
     description:
       name: convert
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.1"
   fake_async:
     dependency: transitive
     description:
       name: fake_async
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   flutter:
@@ -71,28 +71,28 @@ packages:
     dependency: transitive
     description:
       name: matcher
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "0.12.10-nullsafety"
   meta:
     dependency: "direct main"
     description:
       name: meta
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.3.0-nullsafety.2"
   path:
     dependency: transitive
     description:
       name: path
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.8.0-nullsafety"
   quiver:
     dependency: "direct main"
     description:
       name: quiver
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.3"
   sky_engine:
@@ -104,56 +104,56 @@ packages:
     dependency: transitive
     description:
       name: source_span
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.8.0-nullsafety.2"
   stack_trace:
     dependency: transitive
     description:
       name: stack_trace
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.10.0-nullsafety"
   stream_channel:
     dependency: transitive
     description:
       name: stream_channel
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety"
   string_scanner:
     dependency: transitive
     description:
       name: string_scanner
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.1.0-nullsafety"
   term_glyph:
     dependency: transitive
     description:
       name: term_glyph
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.2.0-nullsafety"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "0.2.19-nullsafety"
   typed_data:
     dependency: transitive
     description:
       name: typed_data
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "1.3.0-nullsafety.2"
   vector_math:
     dependency: transitive
     description:
       name: vector_math
-      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub"
+      url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
     source: hosted
     version: "2.1.0-nullsafety.2"
 sdks: