瀏覽代碼

Merge pull request #34 from devmil/feature/async-io-and-terminal-logic

asynchronous file io and terminal logic
xuty 4 年之前
父節點
當前提交
be3ecb8305

+ 61 - 18
example/lib/main.dart

@@ -1,3 +1,5 @@
+import 'dart:async';
+
 import 'package:flutter/material.dart';
 import 'package:xterm/flutter.dart';
 import 'package:xterm/xterm.dart';
@@ -27,39 +29,80 @@ class MyHomePage extends StatefulWidget {
   _MyHomePageState createState() => _MyHomePageState();
 }
 
-class _MyHomePageState extends State<MyHomePage> {
-  Terminal terminal;
+class FakeTerminalBackend implements TerminalBackend {
+  Completer<int> _exitCodeCompleter;
+  // ignore: close_sinks
+  StreamController<String> _outStream;
+
+  FakeTerminalBackend();
 
   @override
-  void initState() {
-    super.initState();
-    terminal = Terminal(
-      onInput: onInput,
-      maxLines: 10000,
-    );
-    terminal.write('xterm.dart demo');
-    terminal.write('\r\n');
-    terminal.write('\$ ');
+  Future<int> get exitCode => _exitCodeCompleter.future;
+
+  @override
+  void init() {
+    _exitCodeCompleter = Completer<int>();
+    _outStream = StreamController<String>();
+    _outStream.sink.add('xterm.dart demo');
+    _outStream.sink.add('\r\n');
+    _outStream.sink.add('\$ ');
   }
 
-  void onInput(String input) {
+  @override
+  Stream<String> get out => _outStream.stream;
+
+  @override
+  void resize(int width, int height) {
+    // NOOP
+  }
+
+  @override
+  void write(String input) {
+    if (input.length <= 0) {
+      return;
+    }
     // in a "real" terminal emulation you would connect onInput to the backend
     // (like a pty or ssh connection) that then handles the changes in the
     // terminal.
     // As we don't have a connected backend here we simulate the changes by
     // directly writing to the terminal.
     if (input == '\r') {
-      terminal.write('\r\n');
-      terminal.write('\$ ');
+      _outStream.sink.add('\r\n');
+      _outStream.sink.add('\$ ');
     } else if (input.codeUnitAt(0) == 127) {
-      terminal.buffer.eraseCharacters(1);
-      terminal.buffer.backspace();
-      terminal.refresh();
+      // Backspace handling
+      _outStream.sink.add('\b \b');
     } else {
-      terminal.write(input);
+      _outStream.sink.add(input);
     }
   }
 
+  @override
+  void terminate() {
+    //NOOP
+  }
+
+  @override
+  void ackProcessed() {
+    //NOOP
+  }
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  TerminalIsolate terminal;
+
+  @override
+  void initState() {
+    super.initState();
+    terminal = TerminalIsolate(
+      backend: FakeTerminalBackend(),
+      maxLines: 10000,
+    );
+    terminal.start();
+  }
+
+  void onInput(String input) {}
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(

+ 61 - 19
example/lib/ssh.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'dart:convert';
 
 import 'package:dartssh/client.dart';
@@ -34,52 +35,93 @@ class MyHomePage extends StatefulWidget {
   _MyHomePageState createState() => _MyHomePageState();
 }
 
-class _MyHomePageState extends State<MyHomePage> {
-  Terminal terminal;
+class SSHTerminalBackend implements TerminalBackend {
   SSHClient client;
 
-  @override
-  void initState() {
-    super.initState();
-    terminal = Terminal(onInput: onInput);
-    connect();
+  String _host;
+  String _username;
+  String _password;
+
+  Completer<int> _exitCodeCompleter;
+  StreamController<String> _outStream;
+
+  SSHTerminalBackend(this._host, this._username, this._password);
+
+  void onWrite(String data) {
+    _outStream.sink.add(data);
   }
 
-  void connect() {
-    terminal.write('connecting $host...');
+  @override
+  Future<int> get exitCode => _exitCodeCompleter.future;
+
+  @override
+  void init() {
+    _exitCodeCompleter = Completer<int>();
+    _outStream = StreamController<String>();
+
+    onWrite('connecting $_host...');
     client = SSHClient(
-      hostport: Uri.parse(host),
-      login: username,
+      hostport: Uri.parse(_host),
+      login: _username,
       print: print,
       termWidth: 80,
       termHeight: 25,
       termvar: 'xterm-256color',
-      getPassword: () => utf8.encode(password),
+      getPassword: () => utf8.encode(_password),
       response: (transport, data) {
-        terminal.write(data);
+        onWrite(data);
       },
       success: () {
-        terminal.write('connected.\n');
+        onWrite('connected.\n');
       },
       disconnected: () {
-        terminal.write('disconnected.');
+        onWrite('disconnected.');
+        _outStream.close();
       },
     );
   }
 
-  void onInput(String input) {
+  @override
+  Stream<String> get out => _outStream.stream;
+
+  @override
+  void resize(int width, int height) {
+    client.setTerminalWindowSize(width, height);
+  }
+
+  @override
+  void write(String input) {
     client?.sendChannelData(utf8.encode(input));
   }
 
+  @override
+  void terminate() {
+    client?.disconnect('terminate');
+  }
+
+  @override
+  void ackProcessed() {
+    // NOOP
+  }
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  Terminal terminal;
+  SSHTerminalBackend backend;
+
+  @override
+  void initState() {
+    super.initState();
+    backend = SSHTerminalBackend(host, username, password);
+    terminal = Terminal(backend: backend, maxLines: 10000);
+  }
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       body: SafeArea(
         child: TerminalView(
           terminal: terminal,
-          onResize: (width, height) {
-            client?.setTerminalWindowSize(width, height);
-          },
         ),
       ),
     );

+ 6 - 2
lib/buffer/buffer.dart

@@ -10,7 +10,10 @@ import 'package:xterm/util/scroll_range.dart';
 import 'package:xterm/util/unicode_v11.dart';
 
 class Buffer {
-  Buffer(this.terminal) {
+  Buffer({
+    required this.terminal,
+    required this.isAltBuffer,
+  }) {
     resetVerticalMargins();
 
     lines = CircularList(
@@ -22,6 +25,7 @@ class Buffer {
   }
 
   final Terminal terminal;
+  final bool isAltBuffer;
   final charset = Charset();
 
   /// lines of the buffer. the length of [lines] should always be equal or
@@ -524,7 +528,7 @@ class Buffer {
     _cursorX = _cursorX.clamp(0, newWidth - 1);
     _cursorY = _cursorY.clamp(0, newHeight - 1);
 
-    if (!terminal.isUsingAltBuffer()) {
+    if (!isAltBuffer) {
       final reflowStrategy = newWidth > oldWidth
           ? ReflowStrategyWider(this)
           : ReflowStrategyNarrower(this);

+ 18 - 15
lib/buffer/buffer_line.dart

@@ -124,8 +124,8 @@ class BufferLine {
   }
 
   void cellClear(int index) {
-    _cells.setInt64(index * _cellSize, 0x00);
-    _cells.setInt64(index * _cellSize + 8, 0x00);
+    _cells.setUint64(index * _cellSize, 0x00);
+    _cells.setUint64(index * _cellSize + 8, 0x00);
   }
 
   void cellInitialize(
@@ -135,11 +135,11 @@ class BufferLine {
     required Cursor cursor,
   }) {
     final cell = index * _cellSize;
-    _cells.setInt32(cell + _cellContent, content);
-    _cells.setInt32(cell + _cellFgColor, cursor.fg);
-    _cells.setInt32(cell + _cellBgColor, cursor.bg);
-    _cells.setInt8(cell + _cellWidth, width);
-    _cells.setInt8(cell + _cellFlags, cursor.flags);
+    _cells.setUint32(cell + _cellContent, content);
+    _cells.setUint32(cell + _cellFgColor, cursor.fg);
+    _cells.setUint32(cell + _cellBgColor, cursor.bg);
+    _cells.setUint8(cell + _cellWidth, width);
+    _cells.setUint8(cell + _cellFlags, cursor.flags);
   }
 
   bool cellHasContent(int index) {
@@ -147,6 +147,9 @@ class BufferLine {
   }
 
   int cellGetContent(int index) {
+    if (index > _maxCols) {
+      return 0;
+    }
     return _cells.getUint32(index * _cellSize + _cellContent);
   }
 
@@ -158,44 +161,44 @@ class BufferLine {
     if (index >= _maxCols) {
       return 0;
     }
-    return _cells.getInt32(index * _cellSize + _cellFgColor);
+    return _cells.getUint32(index * _cellSize + _cellFgColor);
   }
 
   void cellSetFgColor(int index, int color) {
-    _cells.setInt32(index * _cellSize + _cellFgColor, color);
+    _cells.setUint32(index * _cellSize + _cellFgColor, color);
   }
 
   int cellGetBgColor(int index) {
     if (index >= _maxCols) {
       return 0;
     }
-    return _cells.getInt32(index * _cellSize + _cellBgColor);
+    return _cells.getUint32(index * _cellSize + _cellBgColor);
   }
 
   void cellSetBgColor(int index, int color) {
-    _cells.setInt32(index * _cellSize + _cellBgColor, color);
+    _cells.setUint32(index * _cellSize + _cellBgColor, color);
   }
 
   int cellGetFlags(int index) {
     if (index >= _maxCols) {
       return 0;
     }
-    return _cells.getInt8(index * _cellSize + _cellFlags);
+    return _cells.getUint8(index * _cellSize + _cellFlags);
   }
 
   void cellSetFlags(int index, int flags) {
-    _cells.setInt8(index * _cellSize + _cellFlags, flags);
+    _cells.setUint8(index * _cellSize + _cellFlags, flags);
   }
 
   int cellGetWidth(int index) {
     if (index >= _maxCols) {
       return 1;
     }
-    return _cells.getInt8(index * _cellSize + _cellWidth);
+    return _cells.getUint8(index * _cellSize + _cellWidth);
   }
 
   void cellSetWidth(int index, int width) {
-    _cells.setInt8(index * _cellSize + _cellWidth, width);
+    _cells.setUint8(index * _cellSize + _cellWidth, width);
   }
 
   void cellClearFlags(int index) {

+ 4 - 3
lib/frontend/input_behavior.dart

@@ -9,9 +9,10 @@ abstract class InputBehavior {
 
   TextEditingValue get initEditingState;
 
-  void onKeyStroke(RawKeyEvent event, Terminal terminal);
+  void onKeyStroke(RawKeyEvent event, TerminalUiInteraction terminal);
 
-  TextEditingValue? onTextEdit(TextEditingValue value, Terminal terminal);
+  TextEditingValue? onTextEdit(
+      TextEditingValue value, TerminalUiInteraction terminal);
 
-  void onAction(TextInputAction action, Terminal terminal);
+  void onAction(TextInputAction action, TerminalUiInteraction terminal);
 }

+ 5 - 4
lib/frontend/input_behavior_default.dart

@@ -14,7 +14,7 @@ class InputBehaviorDefault extends InputBehavior {
   TextEditingValue get initEditingState => TextEditingValue.empty;
 
   @override
-  void onKeyStroke(RawKeyEvent event, Terminal terminal) {
+  void onKeyStroke(RawKeyEvent event, TerminalUiInteraction terminal) {
     if (event is! RawKeyDownEvent) {
       return;
     }
@@ -33,8 +33,9 @@ class InputBehaviorDefault extends InputBehavior {
   }
 
   @override
-  TextEditingValue? onTextEdit(TextEditingValue value, Terminal terminal) {
-    terminal.onInput(value.text);
+  TextEditingValue? onTextEdit(
+      TextEditingValue value, TerminalUiInteraction terminal) {
+    terminal.raiseOnInput(value.text);
     if (value == TextEditingValue.empty) {
       return null;
     } else {
@@ -43,7 +44,7 @@ class InputBehaviorDefault extends InputBehavior {
   }
 
   @override
-  void onAction(TextInputAction action, Terminal terminal) {
+  void onAction(TextInputAction action, TerminalUiInteraction terminal) {
     //
   }
 }

+ 4 - 3
lib/frontend/input_behavior_mobile.dart

@@ -14,9 +14,10 @@ class InputBehaviorMobile extends InputBehaviorDefault {
     selection: TextSelection.collapsed(offset: 1),
   );
 
-  TextEditingValue onTextEdit(TextEditingValue value, Terminal terminal) {
+  TextEditingValue onTextEdit(
+      TextEditingValue value, TerminalUiInteraction terminal) {
     if (value.text.length > initEditingState.text.length) {
-      terminal.onInput(value.text.substring(1, value.text.length - 1));
+      terminal.raiseOnInput(value.text.substring(1, value.text.length - 1));
     } else if (value.text.length < initEditingState.text.length) {
       terminal.keyInput(TerminalKey.backspace);
     } else {
@@ -30,7 +31,7 @@ class InputBehaviorMobile extends InputBehaviorDefault {
     return initEditingState;
   }
 
-  void onAction(TextInputAction action, Terminal terminal) {
+  void onAction(TextInputAction action, TerminalUiInteraction terminal) {
     print('action $action');
     switch (action) {
       case TextInputAction.done:

+ 55 - 46
lib/frontend/terminal_view.dart

@@ -16,18 +16,15 @@ import 'package:xterm/frontend/input_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/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_style.dart';
 import 'package:xterm/util/bit_flags.dart';
 import 'package:xterm/util/hash_values.dart';
 
-typedef TerminalResizeHandler = void Function(int width, int height);
-
 class TerminalView extends StatefulWidget {
   TerminalView({
     Key? key,
     required this.terminal,
-    this.onResize,
     this.style = const TerminalStyle(),
     this.opacity = 1.0,
     FocusNode? focusNode,
@@ -39,8 +36,7 @@ class TerminalView extends StatefulWidget {
         inputBehavior = inputBehavior ?? InputBehaviors.platform,
         super(key: key ?? ValueKey(terminal));
 
-  final Terminal terminal;
-  final TerminalResizeHandler? onResize;
+  final TerminalUiInteraction terminal;
   final FocusNode focusNode;
   final bool autofocus;
   final ScrollController scrollController;
@@ -105,7 +101,7 @@ class _TerminalViewState extends State<TerminalView> {
 
   void onTerminalChange() {
     _terminalScrollExtent =
-        _cellSize.cellHeight * widget.terminal.buffer.scrollOffsetFromTop;
+        _cellSize.cellHeight * widget.terminal.scrollOffsetFromTop;
 
     if (mounted) {
       setState(() {});
@@ -184,20 +180,23 @@ class _TerminalViewState extends State<TerminalView> {
                 // set viewport height.
                 offset.applyViewportDimension(constraints.maxHeight);
 
-                final minScrollExtent = 0.0;
+                if (widget.terminal.isReady) {
+                  final minScrollExtent = 0.0;
 
-                final maxScrollExtent = math.max(
-                    0.0,
-                    _cellSize.cellHeight * widget.terminal.buffer.height -
-                        constraints.maxHeight);
+                  final maxScrollExtent = math.max(
+                      0.0,
+                      _cellSize.cellHeight * widget.terminal.bufferHeight -
+                          constraints.maxHeight);
 
-                // set how much the terminal can scroll
-                offset.applyContentDimensions(minScrollExtent, maxScrollExtent);
+                  // set how much the terminal can scroll
+                  offset.applyContentDimensions(
+                      minScrollExtent, maxScrollExtent);
 
-                // syncronize pending terminal scroll extent to ScrollController
-                if (_terminalScrollExtent != null) {
-                  position.correctPixels(_terminalScrollExtent!);
-                  _terminalScrollExtent = null;
+                  // syncronize pending terminal scroll extent to ScrollController
+                  if (_terminalScrollExtent != null) {
+                    position.correctPixels(_terminalScrollExtent!);
+                    _terminalScrollExtent = null;
+                  }
                 }
 
                 return buildTerminal(context);
@@ -217,26 +216,26 @@ class _TerminalViewState extends State<TerminalView> {
         print('details : $details');
       },
       onTapDown: (detail) {
-        if (widget.terminal.selection.isEmpty) {
+        if (widget.terminal.selection?.isEmpty ?? true) {
           InputListener.of(context)!.requestKeyboard();
         } else {
-          widget.terminal.selection.clear();
+          widget.terminal.clearSelection();
         }
         final pos = detail.localPosition;
         final offset = getMouseOffset(pos.dx, pos.dy);
-        widget.terminal.mouseMode.onTap(widget.terminal, offset);
+        widget.terminal.onMouseTap(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.onPanStart(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.onPanUpdate(offset);
         widget.terminal.refresh();
       },
       child: Container(
@@ -251,7 +250,7 @@ class _TerminalViewState extends State<TerminalView> {
           ),
         ),
         color:
-            Color(widget.terminal.theme.background).withOpacity(widget.opacity),
+            Color(widget.terminal.backgroundColor).withOpacity(widget.opacity),
       ),
     );
   }
@@ -262,8 +261,8 @@ class _TerminalViewState extends State<TerminalView> {
     final row = (py / _cellSize.cellHeight).floor();
 
     final x = col;
-    final y = widget.terminal.buffer.convertViewLineToRawLine(row) -
-        widget.terminal.buffer.scrollOffsetFromBottom;
+    final y = widget.terminal.convertViewLineToRawLine(row) -
+        widget.terminal.scrollOffsetFromBottom;
 
     return Position(x, y);
   }
@@ -272,6 +271,9 @@ class _TerminalViewState extends State<TerminalView> {
   int? _lastTerminalHeight;
 
   void onSize(double width, double height) {
+    if (!widget.terminal.isReady) {
+      return;
+    }
     final termWidth = (width / _cellSize.cellWidth).floor();
     final termHeight = (height / _cellSize.cellHeight).floor();
 
@@ -282,7 +284,6 @@ class _TerminalViewState extends State<TerminalView> {
     _lastTerminalWidth = termWidth;
     _lastTerminalHeight = termHeight;
 
-    widget.onResize?.call(termWidth, termHeight);
     widget.terminal.resize(termWidth, termHeight);
   }
 
@@ -293,7 +294,7 @@ class _TerminalViewState extends State<TerminalView> {
   void onKeyStroke(RawKeyEvent event) {
     // TODO: find a way to stop scrolling immediately after key stroke.
     widget.inputBehavior.onKeyStroke(event, widget.terminal);
-    widget.terminal.buffer.setScrollOffsetFromBottom(0);
+    widget.terminal.setScrollOffsetFromBottom(0);
   }
 
   void onFocus(bool focused) {
@@ -310,10 +311,7 @@ class _TerminalViewState extends State<TerminalView> {
   void onScroll(double offset) {
     final topOffset = (offset / _cellSize.cellHeight).ceil();
     final bottomOffset = widget.terminal.invisibleHeight - topOffset;
-
-    setState(() {
-      widget.terminal.buffer.setScrollOffsetFromBottom(bottomOffset);
-    });
+    widget.terminal.setScrollOffsetFromBottom(bottomOffset);
   }
 }
 
@@ -326,7 +324,7 @@ class TerminalPainter extends CustomPainter {
     required this.charSize,
   });
 
-  final Terminal terminal;
+  final TerminalUiInteraction terminal;
   final TerminalView view;
   final Oscillator oscillator;
   final bool focused;
@@ -334,6 +332,9 @@ class TerminalPainter extends CustomPainter {
 
   @override
   void paint(Canvas canvas, Size size) {
+    if (!terminal.isReady) {
+      return;
+    }
     _paintBackground(canvas);
 
     // if (oscillator.value) {
@@ -355,7 +356,7 @@ class TerminalPainter extends CustomPainter {
       final line = lines[row];
       final offsetY = row * charSize.cellHeight;
       // final cellCount = math.min(terminal.viewWidth, line.length);
-      final cellCount = terminal.viewWidth;
+      final cellCount = terminal.terminalWidth;
 
       for (var col = 0; col < cellCount; col++) {
         final cellWidth = line.cellGetWidth(col);
@@ -373,6 +374,11 @@ class TerminalPainter extends CustomPainter {
           continue;
         }
 
+        // when a program reports black as background then it "really" means transparent
+        if (effectBgColor == 0xFF000000) {
+          continue;
+        }
+
         // final cellFlags = line.cellGetFlags(i);
         // final cell = line.getCell(i);
         // final attr = cell.attr;
@@ -395,19 +401,22 @@ class TerminalPainter extends CustomPainter {
   }
 
   void _paintSelection(Canvas canvas) {
+    final selection = terminal.selection;
+    if (selection == null) {
+      return;
+    }
     final paint = Paint()..color = Colors.white.withOpacity(0.3);
 
-    for (var y = 0; y < terminal.viewHeight; y++) {
+    for (var y = 0; y < terminal.terminalHeight; y++) {
       final offsetY = y * charSize.cellHeight;
-      final absoluteY = terminal.buffer.convertViewLineToRawLine(y) -
-          terminal.buffer.scrollOffsetFromBottom;
+      final absoluteY = terminal.convertViewLineToRawLine(y) -
+          terminal.scrollOffsetFromBottom;
 
-      for (var x = 0; x < terminal.viewWidth; x++) {
+      for (var x = 0; x < terminal.terminalWidth; x++) {
         var cellCount = 0;
 
-        while (
-            terminal.selection.contains(Position(x + cellCount, absoluteY)) &&
-                x + cellCount < terminal.viewWidth) {
+        while (selection.contains(Position(x + cellCount, absoluteY)) &&
+            x + cellCount < terminal.terminalWidth) {
           cellCount++;
         }
 
@@ -436,7 +445,7 @@ class TerminalPainter extends CustomPainter {
       final line = lines[row];
       final offsetY = row * charSize.cellHeight;
       // final cellCount = math.min(terminal.viewWidth, line.length);
-      final cellCount = terminal.viewWidth;
+      final cellCount = terminal.terminalWidth;
 
       for (var col = 0; col < cellCount; col++) {
         final width = line.cellGetWidth(col);
@@ -527,18 +536,18 @@ class TerminalPainter extends CustomPainter {
   }
 
   void _paintCursor(Canvas canvas) {
-    final screenCursorY = terminal.cursorY + terminal.scrollOffset;
-    if (screenCursorY < 0 || screenCursorY >= terminal.viewHeight) {
+    final screenCursorY = terminal.cursorY + terminal.scrollOffsetFromBottom;
+    if (screenCursorY < 0 || screenCursorY >= terminal.terminalHeight) {
       return;
     }
 
     final width = charSize.cellWidth *
-        terminal.buffer.currentLine.cellGetWidth(terminal.cursorX).clamp(1, 2);
+        (terminal.currentLine?.cellGetWidth(terminal.cursorX).clamp(1, 2) ?? 1);
 
     final offsetX = charSize.cellWidth * terminal.cursorX;
     final offsetY = charSize.cellHeight * screenCursorY;
     final paint = Paint()
-      ..color = Color(terminal.theme.cursor)
+      ..color = Color(terminal.cursorColor)
       ..strokeWidth = focused ? 0.0 : 1.0
       ..style = focused ? PaintingStyle.fill : PaintingStyle.stroke;
 

+ 3 - 3
lib/mouse/mouse_mode.dart

@@ -24,12 +24,12 @@ class MouseModeNone extends MouseMode {
 
   @override
   void onPanStart(Terminal terminal, Position offset) {
-    terminal.selection.init(offset);
+    terminal.selection!.init(offset);
   }
 
   @override
   void onPanUpdate(Terminal terminal, Position offset) {
-    terminal.selection.update(offset);
+    terminal.selection!.update(offset);
   }
 }
 
@@ -49,6 +49,6 @@ class MouseModeX10 extends MouseMode {
     buffer.writeCharCode(btn + 32);
     buffer.writeCharCode(px + 32);
     buffer.writeCharCode(py + 32);
-    terminal.onInput(buffer.toString());
+    terminal.backend?.write(buffer.toString());
   }
 }

+ 4 - 3
lib/terminal/csi.dart

@@ -306,10 +306,11 @@ void csiDeviceStatusReportHandler(CSI csi, Terminal terminal) {
 
   switch (csi.params[0]) {
     case 5:
-      terminal.onInput("\x1b[0n");
+      terminal.backend?.write("\x1b[0n");
       break;
     case 6: // report cursor position
-      terminal.onInput("\x1b[${terminal.cursorX + 1};${terminal.cursorY + 1}R");
+      terminal.backend
+          ?.write("\x1b[${terminal.cursorX + 1};${terminal.cursorY + 1}R");
       break;
     default:
       terminal.debug
@@ -325,7 +326,7 @@ void csiSendDeviceAttributesHandler(CSI csi, Terminal terminal) {
     response = '>0;0;0';
   }
 
-  terminal.onInput('\x1b[${response}c');
+  terminal.backend?.write('\x1b[${response}c');
 }
 
 void csiCursorUpHandler(CSI csi, Terminal terminal) {

+ 125 - 19
lib/terminal/terminal.dart

@@ -1,8 +1,10 @@
+import 'dart:async';
 import 'dart:collection';
 import 'dart:math' show max, min;
 
 import 'package:xterm/buffer/buffer.dart';
 import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/mouse/position.dart';
 import 'package:xterm/mouse/selection.dart';
 import 'package:xterm/input/keys.dart';
 import 'package:xterm/input/keytab/keytab.dart';
@@ -14,6 +16,8 @@ import 'package:xterm/terminal/cursor.dart';
 import 'package:xterm/terminal/platform.dart';
 import 'package:xterm/terminal/sbc.dart';
 import 'package:xterm/terminal/tabs.dart';
+import 'package:xterm/terminal/terminal_backend.dart';
+import 'package:xterm/terminal/terminal_ui_interaction.dart';
 import 'package:xterm/theme/terminal_color.dart';
 import 'package:xterm/theme/terminal_theme.dart';
 import 'package:xterm/theme/terminal_themes.dart';
@@ -25,14 +29,13 @@ typedef BellHandler = void Function();
 typedef TitleChangeHandler = void Function(String);
 typedef IconChangeHandler = void Function(String);
 
-void _defaultInputHandler(String _) {}
 void _defaultBellHandler() {}
 void _defaultTitleHandler(String _) {}
 void _defaultIconHandler(String _) {}
 
-class Terminal with Observable {
+class Terminal with Observable implements TerminalUiInteraction {
   Terminal({
-    this.onInput = _defaultInputHandler,
+    this.backend,
     this.onBell = _defaultBellHandler,
     this.onTitleChange = _defaultTitleHandler,
     this.onIconChange = _defaultIconHandler,
@@ -40,8 +43,14 @@ class Terminal with Observable {
     this.theme = TerminalThemes.defaultTheme,
     required int maxLines,
   }) : _maxLines = maxLines {
-    _mainBuffer = Buffer(this);
-    _altBuffer = Buffer(this);
+    backend?.init();
+    backend?.exitCode.then((value) {
+      _isTerminated = true;
+      _backendExited.complete(value);
+    });
+    backend?.out.listen(write);
+    _mainBuffer = Buffer(terminal: this, isAltBuffer: false);
+    _altBuffer = Buffer(terminal: this, isAltBuffer: true);
     _buffer = _mainBuffer;
 
     cursor = Cursor(
@@ -178,11 +187,11 @@ class Terminal with Observable {
   late final Cursor cursor;
 
   final keytab = Keytab.defaultKeytab();
-  final selection = Selection();
+  final _selection = Selection();
   final tabs = Tabs();
   final debug = DebugHandler();
 
-  final TerminalInputHandler onInput;
+  final TerminalBackend? backend;
   final BellHandler onBell;
   final TitleChangeHandler onTitleChange;
   final IconChangeHandler onIconChange;
@@ -194,7 +203,15 @@ class Terminal with Observable {
 
   int get cursorX => buffer.cursorX;
   int get cursorY => buffer.cursorY;
-  int get scrollOffset => buffer.scrollOffsetFromBottom;
+
+  void setScrollOffsetFromBottom(int scrollOffset) {
+    final oldOffset = _buffer.scrollOffsetFromBottom;
+    _buffer.setScrollOffsetFromBottom(scrollOffset);
+    if (oldOffset != scrollOffset) {
+      _dirty = true;
+      refresh();
+    }
+  }
 
   /// Writes data to the terminal. Terminal sequences and special characters are
   /// interpreted.
@@ -203,6 +220,7 @@ class Terminal with Observable {
   void write(String text) {
     _queue.addAll(text.runes);
     _processInput();
+    backend?.ackProcessed();
     refresh();
   }
 
@@ -340,6 +358,7 @@ class Terminal with Observable {
   /// than 0. Text reflow is currently not implemented and will be avaliable in
   /// the future.
   void resize(int newWidth, int newHeight) {
+    backend?.resize(newWidth, newHeight);
     newWidth = max(newWidth, 1);
     newHeight = max(newHeight, 1);
 
@@ -348,7 +367,9 @@ class Terminal with Observable {
     _viewWidth = newWidth;
     _viewHeight = newHeight;
 
-    buffer.resize(oldWidth, oldHeight, newWidth, newHeight);
+    //we need to resize both buffers so that they are ready when we switch between them
+    _altBuffer.resize(oldWidth, oldHeight, newWidth, newHeight);
+    _mainBuffer.resize(oldWidth, oldHeight, newWidth, newHeight);
 
     // maybe reflow should happen here.
     if (buffer == _altBuffer) {
@@ -420,7 +441,7 @@ class Terminal with Observable {
       if (record.action.type == KeytabActionType.input) {
         debug.onMsg('input: ${record.action.value}');
         final input = keytabUnescape(record.action.value);
-        onInput(input);
+        backend?.write(input);
         return;
       }
     }
@@ -429,7 +450,7 @@ class Terminal with Observable {
       if (key.index >= TerminalKey.keyA.index &&
           key.index <= TerminalKey.keyZ.index) {
         final input = key.index - TerminalKey.keyA.index + 1;
-        onInput(String.fromCharCode(input));
+        backend?.write(String.fromCharCode(input));
         return;
       }
     }
@@ -438,20 +459,20 @@ class Terminal with Observable {
       if (key.index >= TerminalKey.keyA.index &&
           key.index <= TerminalKey.keyZ.index) {
         final input = [0x1b, key.index - TerminalKey.keyA.index + 65];
-        onInput(String.fromCharCodes(input));
+        backend?.write(String.fromCharCodes(input));
         return;
       }
     }
   }
 
   String? getSelectedText() {
-    if (selection.isEmpty) {
+    if (_selection.isEmpty) {
       return null;
     }
 
     final builder = StringBuffer();
 
-    for (var row = selection.start!.y; row <= selection.end!.y; row++) {
+    for (var row = _selection.start!.y; row <= _selection.end!.y; row++) {
       if (row >= buffer.height) {
         break;
       }
@@ -461,14 +482,14 @@ class Terminal with Observable {
       var xStart = 0;
       var xEnd = viewWidth - 1;
 
-      if (row == selection.start!.y) {
-        xStart = selection.start!.x;
+      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;
+      if (row == _selection.end!.y) {
+        xEnd = _selection.end!.x;
       }
 
       for (var col = xStart; col <= xEnd; col++) {
@@ -499,7 +520,7 @@ class Terminal with Observable {
       data = '\x1b[200~$data\x1b[201~';
     }
 
-    onInput(data);
+    backend?.write(data);
   }
 
   void selectWord(int x, int y) {}
@@ -531,4 +552,89 @@ class Terminal with Observable {
       }
     }
   }
+
+  @override
+  int get backgroundColor => theme.background;
+
+  @override
+  int get bufferHeight => buffer.height;
+
+  @override
+  void clearSelection() {
+    selection?.clear();
+  }
+
+  @override
+  int convertViewLineToRawLine(int viewLine) {
+    if (viewHeight > buffer.height) {
+      return viewLine;
+    }
+
+    return viewLine + (buffer.height - viewHeight);
+  }
+
+  @override
+  BufferLine? get currentLine => buffer.currentLine;
+
+  @override
+  int get cursorColor => theme.cursor;
+
+  @override
+  String? get selectedText => getSelectedText();
+
+  @override
+  bool get isReady => true;
+
+  @override
+  void onMouseTap(Position position) {
+    mouseMode.onTap(this, position);
+  }
+
+  @override
+  void onPanStart(Position position) {
+    mouseMode.onPanStart(this, position);
+  }
+
+  @override
+  void onPanUpdate(Position position) {
+    mouseMode.onPanUpdate(this, position);
+  }
+
+  @override
+  int get scrollOffsetFromBottom => buffer.scrollOffsetFromBottom;
+
+  @override
+  int get scrollOffsetFromTop => buffer.scrollOffsetFromTop;
+
+  @override
+  int get terminalHeight => viewHeight;
+
+  @override
+  int get terminalWidth => viewWidth;
+
+  @override
+  Selection? get selection => _selection;
+
+  @override
+  void raiseOnInput(String input) {
+    backend?.write(input);
+  }
+
+  final _backendExited = Completer<int>();
+  @override
+  Future<int> get backendExited => _backendExited.future;
+
+  var _isTerminated = false;
+
+  @override
+  void terminateBackend() {
+    if (_isTerminated) {
+      return;
+    }
+    _isTerminated = true;
+    backend?.terminate();
+  }
+
+  @override
+  bool get isTerminated => _isTerminated;
 }

+ 31 - 0
lib/terminal/terminal_backend.dart

@@ -0,0 +1,31 @@
+import 'package:xterm/terminal/terminal_isolate.dart';
+
+/// interface for every Terminal backend
+abstract class TerminalBackend {
+  /// initializes the backend
+  /// This can be used to instantiate instances that are problematic when
+  /// passed to a Isolate.
+  /// The [TerminalIsolate] will pass the backend to the [Terminal] that then
+  /// executes [init] from inside the Isolate.
+  /// So when your backend needs any complex instances (most of them will)
+  /// then strongly consider instantiating them here
+  void init();
+
+  /// Stream for data that gets read from the backend
+  Stream<String> get out;
+
+  /// Future that fires when the backend terminates
+  Future<int> get exitCode;
+
+  /// writes data to this backend
+  void write(String input);
+
+  /// notifies the backend about a view port resize that happened
+  void resize(int width, int height);
+
+  /// terminates this backend
+  void terminate();
+
+  /// acknowledges processing of a data junk
+  void ackProcessed();
+}

+ 473 - 0
lib/terminal/terminal_isolate.dart

@@ -0,0 +1,473 @@
+import 'dart:async';
+import 'dart:isolate';
+
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/input/keys.dart';
+import 'package:xterm/mouse/position.dart';
+import 'package:xterm/mouse/selection.dart';
+import 'package:xterm/terminal/platform.dart';
+import 'package:xterm/terminal/terminal.dart';
+import 'package:xterm/terminal/terminal_backend.dart';
+import 'package:xterm/terminal/terminal_ui_interaction.dart';
+import 'package:xterm/theme/terminal_theme.dart';
+import 'package:xterm/theme/terminal_themes.dart';
+import 'package:xterm/util/event_debouncer.dart';
+import 'package:xterm/util/observable.dart';
+
+enum _IsolateCommand {
+  sendPort,
+  init,
+  write,
+  refresh,
+  clearSelection,
+  mouseTap,
+  mousePanStart,
+  mousePanUpdate,
+  setScrollOffsetFromTop,
+  resize,
+  onInput,
+  keyInput,
+  requestNewStateWhenDirty,
+  paste,
+  terminateBackend
+}
+
+enum _IsolateEvent {
+  titleChanged,
+  iconChanged,
+  bell,
+  notifyChange,
+  newState,
+  exit,
+}
+
+/// main entry for the terminal isolate
+void terminalMain(SendPort port) async {
+  final rp = ReceivePort();
+  port.send(rp.sendPort);
+
+  Terminal? _terminal;
+  var _needNotify = true;
+
+  await for (var msg in rp) {
+    // process incoming commands
+    final _IsolateCommand action = msg[0];
+    switch (action) {
+      case _IsolateCommand.sendPort:
+        port = msg[1];
+        break;
+      case _IsolateCommand.init:
+        final TerminalInitData initData = msg[1];
+        _terminal = Terminal(
+            backend: initData.backend,
+            onTitleChange: (String title) {
+              port.send([_IsolateEvent.titleChanged, title]);
+            },
+            onIconChange: (String icon) {
+              port.send([_IsolateEvent.iconChanged, icon]);
+            },
+            onBell: () {
+              port.send([_IsolateEvent.bell]);
+            },
+            platform: initData.platform,
+            theme: initData.theme,
+            maxLines: initData.maxLines);
+        _terminal.addListener(() {
+          if (_needNotify) {
+            port.send([_IsolateEvent.notifyChange]);
+            _needNotify = false;
+          }
+        });
+        _terminal.backendExited
+            .then((value) => port.send([_IsolateEvent.exit, value]));
+        port.send([_IsolateEvent.notifyChange]);
+        break;
+      case _IsolateCommand.write:
+        _terminal?.write(msg[1]);
+        break;
+      case _IsolateCommand.refresh:
+        _terminal?.refresh();
+        break;
+      case _IsolateCommand.clearSelection:
+        _terminal?.selection!.clear();
+        break;
+      case _IsolateCommand.mouseTap:
+        _terminal?.mouseMode.onTap(_terminal, msg[1]);
+        break;
+      case _IsolateCommand.mousePanStart:
+        _terminal?.mouseMode.onPanStart(_terminal, msg[1]);
+        break;
+      case _IsolateCommand.mousePanUpdate:
+        _terminal?.mouseMode.onPanUpdate(_terminal, msg[1]);
+        break;
+      case _IsolateCommand.setScrollOffsetFromTop:
+        _terminal?.setScrollOffsetFromBottom(msg[1]);
+        break;
+      case _IsolateCommand.resize:
+        _terminal?.resize(msg[1], msg[2]);
+        break;
+      case _IsolateCommand.onInput:
+        _terminal?.backend?.write(msg[1]);
+        break;
+      case _IsolateCommand.keyInput:
+        _terminal?.keyInput(msg[1],
+            ctrl: msg[2], alt: msg[3], shift: msg[4], mac: msg[5]);
+        break;
+      case _IsolateCommand.requestNewStateWhenDirty:
+        if (_terminal == null) {
+          break;
+        }
+        if (_terminal.dirty) {
+          final newState = TerminalState(
+            _terminal.scrollOffsetFromBottom,
+            _terminal.scrollOffsetFromTop,
+            _terminal.buffer.height,
+            _terminal.invisibleHeight,
+            _terminal.viewHeight,
+            _terminal.viewWidth,
+            _terminal.selection!,
+            _terminal.getSelectedText(),
+            _terminal.theme.background,
+            _terminal.cursorX,
+            _terminal.cursorY,
+            _terminal.showCursor,
+            _terminal.theme.cursor,
+            _terminal.getVisibleLines(),
+          );
+          port.send([_IsolateEvent.newState, newState]);
+          _needNotify = true;
+        }
+        break;
+      case _IsolateCommand.paste:
+        _terminal?.paste(msg[1]);
+        break;
+      case _IsolateCommand.terminateBackend:
+        _terminal?.terminateBackend();
+    }
+  }
+}
+
+/// This class holds the initialization data needed for the Terminal.
+/// This data has to be passed from the UI Isolate where the TerminalIsolate
+/// class gets instantiated into the Isolate that will run the Terminal.
+class TerminalInitData {
+  PlatformBehavior platform;
+  TerminalTheme theme;
+  int maxLines;
+  TerminalBackend? backend;
+  TerminalInitData(this.backend, this.platform, this.theme, this.maxLines);
+}
+
+/// This class holds a complete TerminalState as needed by the UI.
+/// The state held here is self-contained and has no dependencies to the source
+/// Terminal. Therefore it can be safely transferred across Isolate boundaries.
+class TerminalState {
+  int scrollOffsetFromTop;
+  int scrollOffsetFromBottom;
+
+  int bufferHeight;
+  int invisibleHeight;
+
+  int viewHeight;
+  int viewWidth;
+
+  Selection selection;
+  String? selectedText;
+
+  int backgroundColor;
+
+  int cursorX;
+  int cursorY;
+  bool showCursor;
+  int cursorColor;
+
+  List<BufferLine> visibleLines;
+
+  bool consumed = false;
+
+  TerminalState(
+    this.scrollOffsetFromBottom,
+    this.scrollOffsetFromTop,
+    this.bufferHeight,
+    this.invisibleHeight,
+    this.viewHeight,
+    this.viewWidth,
+    this.selection,
+    this.selectedText,
+    this.backgroundColor,
+    this.cursorX,
+    this.cursorY,
+    this.showCursor,
+    this.cursorColor,
+    this.visibleLines,
+  );
+}
+
+void _defaultBellHandler() {}
+void _defaultTitleHandler(String _) {}
+void _defaultIconHandler(String _) {}
+
+/// The TerminalIsolate class hosts an Isolate that runs a Terminal.
+/// It handles all the communication with and from the Terminal and implements
+/// [TerminalUiInteraction] as well as the terminal and therefore can simply
+/// be exchanged with a Terminal.
+/// This class is the preferred use of a Terminal as the Terminal logic and all
+/// the communication with the backend are happening outside the UI thread.
+///
+/// There is a special constraints in using this class:
+/// The given backend has to be built so that it can be passed into an Isolate.
+///
+/// This means in particular that it is not allowed to have any closures in its
+/// object graph.
+/// It is a good idea to move as much instantiation as possible into the
+/// [TerminalBackend.init] method that gets called after the backend instance
+/// has been passed and is therefore allowed to instantiate parts of the object
+/// graph that do contain closures.
+class TerminalIsolate with Observable implements TerminalUiInteraction {
+  final _receivePort = ReceivePort();
+  SendPort? _sendPort;
+  late Isolate _isolate;
+
+  final TerminalBackend? backend;
+  final BellHandler onBell;
+  final TitleChangeHandler onTitleChange;
+  final IconChangeHandler onIconChange;
+  final PlatformBehavior _platform;
+
+  final TerminalTheme theme;
+  final int maxLines;
+
+  final Duration minRefreshDelay;
+  final EventDebouncer _refreshEventDebouncer;
+
+  TerminalState? _lastState;
+
+  TerminalState? get lastState {
+    return _lastState;
+  }
+
+  TerminalIsolate({
+    this.backend,
+    this.onBell = _defaultBellHandler,
+    this.onTitleChange = _defaultTitleHandler,
+    this.onIconChange = _defaultIconHandler,
+    PlatformBehavior platform = PlatformBehaviors.unix,
+    this.theme = TerminalThemes.defaultTheme,
+    this.minRefreshDelay = const Duration(milliseconds: 16),
+    required this.maxLines,
+  })   : _platform = platform,
+        _refreshEventDebouncer = EventDebouncer(minRefreshDelay);
+
+  @override
+  int get scrollOffsetFromBottom => _lastState!.scrollOffsetFromBottom;
+
+  @override
+  int get scrollOffsetFromTop => _lastState!.scrollOffsetFromTop;
+
+  @override
+  int get bufferHeight => _lastState!.bufferHeight;
+
+  @override
+  int get terminalHeight => _lastState!.viewHeight;
+
+  @override
+  int get terminalWidth => _lastState!.viewWidth;
+
+  @override
+  int get invisibleHeight => _lastState!.invisibleHeight;
+
+  @override
+  Selection? get selection => _lastState?.selection;
+
+  @override
+  bool get showCursor => _lastState?.showCursor ?? true;
+
+  @override
+  List<BufferLine> getVisibleLines() {
+    if (_lastState == null) {
+      return List<BufferLine>.empty();
+    }
+    return _lastState!.visibleLines;
+  }
+
+  @override
+  int get cursorY => _lastState?.cursorY ?? 0;
+
+  @override
+  int get cursorX => _lastState?.cursorX ?? 0;
+
+  @override
+  BufferLine? get currentLine {
+    if (_lastState == null) {
+      return null;
+    }
+
+    int visibleLineIndex =
+        _lastState!.cursorY - _lastState!.scrollOffsetFromTop;
+    if (visibleLineIndex < 0) {
+      visibleLineIndex = _lastState!.cursorY;
+    }
+    return _lastState!.visibleLines[visibleLineIndex];
+  }
+
+  @override
+  int get cursorColor => _lastState?.cursorColor ?? 0;
+
+  @override
+  int get backgroundColor => _lastState?.backgroundColor ?? 0;
+
+  @override
+  bool get dirty {
+    if (_lastState == null) {
+      return false;
+    }
+    if (_lastState!.consumed) {
+      return false;
+    }
+    _lastState!.consumed = true;
+    return true;
+  }
+
+  @override
+  PlatformBehavior get platform => _platform;
+
+  @override
+  String? get selectedText => _lastState?.selectedText;
+
+  @override
+  bool get isReady => _lastState != null;
+
+  Future<void> start() async {
+    final initialRefreshCompleted = Completer<bool>();
+    var firstReceivePort = ReceivePort();
+    _isolate = await Isolate.spawn(terminalMain, firstReceivePort.sendPort);
+    _sendPort = await firstReceivePort.first;
+    firstReceivePort.close();
+    _sendPort!.send([_IsolateCommand.sendPort, _receivePort.sendPort]);
+    _receivePort.listen((message) {
+      _IsolateEvent action = message[0];
+      switch (action) {
+        case _IsolateEvent.bell:
+          this.onBell();
+          break;
+        case _IsolateEvent.titleChanged:
+          this.onTitleChange(message[1]);
+          break;
+        case _IsolateEvent.iconChanged:
+          this.onIconChange(message[1]);
+          break;
+        case _IsolateEvent.notifyChange:
+          _refreshEventDebouncer.notifyEvent(() {
+            poll();
+          });
+          break;
+        case _IsolateEvent.newState:
+          _lastState = message[1];
+          if (!initialRefreshCompleted.isCompleted) {
+            initialRefreshCompleted.complete(true);
+          }
+          this.notifyListeners();
+          break;
+        case _IsolateEvent.exit:
+          _isTerminated = true;
+          _backendExited.complete(message[1]);
+          break;
+      }
+    });
+    _sendPort!.send([
+      _IsolateCommand.init,
+      TerminalInitData(this.backend, this.platform, this.theme, this.maxLines)
+    ]);
+    await initialRefreshCompleted.future;
+  }
+
+  void stop() {
+    terminateBackend();
+    _isolate.kill();
+  }
+
+  void poll() {
+    _sendPort?.send([_IsolateCommand.requestNewStateWhenDirty]);
+  }
+
+  void refresh() {
+    _sendPort?.send([_IsolateCommand.refresh]);
+  }
+
+  void clearSelection() {
+    _sendPort?.send([_IsolateCommand.clearSelection]);
+  }
+
+  void onMouseTap(Position position) {
+    _sendPort?.send([_IsolateCommand.mouseTap, position]);
+  }
+
+  void onPanStart(Position position) {
+    _sendPort?.send([_IsolateCommand.mousePanStart, position]);
+  }
+
+  void onPanUpdate(Position position) {
+    _sendPort?.send([_IsolateCommand.mousePanUpdate, position]);
+  }
+
+  void setScrollOffsetFromBottom(int offset) {
+    _sendPort?.send([_IsolateCommand.setScrollOffsetFromTop, offset]);
+  }
+
+  int convertViewLineToRawLine(int viewLine) {
+    if (_lastState == null) {
+      return 0;
+    }
+
+    if (_lastState!.viewHeight > _lastState!.bufferHeight) {
+      return viewLine;
+    }
+
+    return viewLine + (_lastState!.bufferHeight - _lastState!.viewHeight);
+  }
+
+  void write(String text) {
+    _sendPort?.send([_IsolateCommand.write, text]);
+  }
+
+  void paste(String data) {
+    _sendPort?.send([_IsolateCommand.paste, data]);
+  }
+
+  void resize(int newWidth, int newHeight) {
+    _sendPort?.send([_IsolateCommand.resize, newWidth, newHeight]);
+  }
+
+  void raiseOnInput(String text) {
+    _sendPort?.send([_IsolateCommand.onInput, text]);
+  }
+
+  void keyInput(
+    TerminalKey key, {
+    bool ctrl = false,
+    bool alt = false,
+    bool shift = false,
+    bool mac = false,
+    // bool meta,
+  }) {
+    _sendPort?.send([_IsolateCommand.keyInput, key, ctrl, alt, shift, mac]);
+  }
+
+  var _isTerminated = false;
+
+  final _backendExited = Completer<int>();
+  @override
+  Future<int> get backendExited => _backendExited.future;
+
+  @override
+  void terminateBackend() {
+    if (_isTerminated) {
+      return;
+    }
+    _isTerminated = true;
+    _sendPort?.send([_IsolateCommand.terminateBackend]);
+  }
+
+  @override
+  bool get isTerminated => _isTerminated;
+}

+ 118 - 0
lib/terminal/terminal_ui_interaction.dart

@@ -0,0 +1,118 @@
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/input/keys.dart';
+import 'package:xterm/mouse/position.dart';
+import 'package:xterm/mouse/selection.dart';
+import 'package:xterm/terminal/platform.dart';
+import 'package:xterm/util/observable.dart';
+
+/// this interface describes what a Terminal UI needs from a Terminal
+abstract class TerminalUiInteraction with Observable {
+  /// the ViewPort scroll offset from the bottom
+  int get scrollOffsetFromBottom;
+
+  /// the ViewPort scroll offset from the top
+  int get scrollOffsetFromTop;
+
+  /// the total buffer height
+  int get bufferHeight;
+
+  /// terminal height (view port)
+  int get terminalHeight;
+
+  /// terminal width (view port)
+  int get terminalWidth;
+
+  /// the part of the buffer that is not visible (scrollback)
+  int get invisibleHeight;
+
+  /// object that describes details about the current selection
+  Selection? get selection;
+
+  /// [true] when the cursor shall be shown, otherwise [false]
+  bool get showCursor;
+
+  /// returns the visible lines
+  List<BufferLine> getVisibleLines();
+
+  /// cursor y coordinate
+  int get cursorY;
+
+  /// cursor x coordinate
+  int get cursorX;
+
+  /// current line
+  BufferLine? get currentLine;
+
+  /// color code for the cursor
+  int get cursorColor;
+
+  /// color code for the background
+  int get backgroundColor;
+
+  /// flag that indicates if the terminal is dirty (since the last time this
+  /// flag has been queried)
+  bool get dirty;
+
+  /// platform behavior for this terminal
+  PlatformBehavior get platform;
+
+  /// selected text defined by [selection]
+  String? get selectedText;
+
+  /// flag that indicates if the Terminal is ready
+  bool get isReady;
+
+  /// refreshes the Terminal (notifies listeners and sets it to dirty)
+  void refresh();
+
+  /// clears the selection
+  void clearSelection();
+
+  /// notify the Terminal about a mouse tap
+  void onMouseTap(Position position);
+
+  /// notify the Terminal about a pan start
+  void onPanStart(Position position);
+
+  /// notify the Terminal about a pan update
+  void onPanUpdate(Position position);
+
+  /// sets the scroll offset from bottom (scrolling)
+  void setScrollOffsetFromBottom(int offset);
+
+  /// converts the given view line (view port line) index to its position in the
+  /// overall buffer
+  int convertViewLineToRawLine(int viewLine);
+
+  /// notifies the Terminal about user input
+  void raiseOnInput(String input);
+
+  /// writes data to the Terminal
+  void write(String text);
+
+  /// paste clipboard content to the Terminal
+  void paste(String data);
+
+  /// notifies the Terminal about a resize that happened. The Terminal will
+  /// do any resize / reflow logic and notify the backend about the resize
+  void resize(int newWidth, int newHeight);
+
+  /// notifies the Terminal about key input
+  void keyInput(
+    TerminalKey key, {
+    bool ctrl = false,
+    bool alt = false,
+    bool shift = false,
+    bool mac = false,
+    // bool meta,
+  });
+
+  /// Future that fires when the backend has exited
+  Future<int> get backendExited;
+
+  /// terminates the backend. If already terminated, nothing happens
+  void terminateBackend();
+
+  /// flag that indicates if the backend is already terminated
+  bool get isTerminated;
+}

+ 39 - 0
lib/util/event_debouncer.dart

@@ -0,0 +1,39 @@
+import 'dart:async';
+
+/// EventDebouncer makes sure that events aren't fired at a higher frequency
+/// than specified.
+/// To ensure that EventDebouncer will ignore events that happen in between
+/// and just call the latest event that happened.
+class EventDebouncer {
+  final Duration _debounceDuration;
+  Timer? _debounceTimer;
+  Function? _latestCallback;
+
+  EventDebouncer(this._debounceDuration);
+
+  void _consumeLatestCallback() {
+    if (!(_debounceTimer?.isActive ?? false)) {
+      _debounceTimer = null;
+    }
+
+    if (_latestCallback == null) {
+      return;
+    }
+
+    if (_debounceTimer == null) {
+      _latestCallback!();
+      _latestCallback = null;
+      _debounceTimer = Timer(
+        _debounceDuration,
+        () {
+          _consumeLatestCallback();
+        },
+      );
+    }
+  }
+
+  void notifyEvent(Function callback) {
+    _latestCallback = callback;
+    _consumeLatestCallback();
+  }
+}

+ 3 - 0
lib/xterm.dart

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