Преглед на файлове

Merge pull request #13 from devmil/feature/reflow

Port of the Reflow Logic
xuty преди 4 години
родител
ревизия
4200f0eb99

+ 45 - 15
lib/buffer/buffer.dart

@@ -1,6 +1,8 @@
 import 'dart:math' show max, min;
 
 import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/reflow_strategy_narrower.dart';
+import 'package:xterm/buffer/reflow_strategy_wider.dart';
 import 'package:xterm/terminal/charset.dart';
 import 'package:xterm/terminal/terminal.dart';
 import 'package:xterm/utli/circular_list.dart';
@@ -87,6 +89,9 @@ class Buffer {
     if (_cursorX >= terminal.viewWidth) {
       newLine();
       setCursorX(0);
+      if (terminal.autoWrapMode) {
+        currentLine.isWrapped = true;
+      }
     }
 
     final line = currentLine;
@@ -153,6 +158,7 @@ class Buffer {
 
   void backspace() {
     if (_cursorX == 0 && currentLine.isWrapped) {
+      currentLine.isWrapped = false;
       movePosition(terminal.viewWidth - 1, -1);
     } else if (_cursorX == terminal.viewWidth) {
       movePosition(-2, 0);
@@ -182,7 +188,9 @@ class Buffer {
     eraseLineFromCursor();
 
     for (var i = _cursorY + 1; i < terminal.viewHeight; i++) {
-      getViewLine(i).erase(terminal.cursor, 0, terminal.viewWidth);
+      final line = getViewLine(i);
+      line.isWrapped = false;
+      line.erase(terminal.cursor, 0, terminal.viewWidth);
     }
   }
 
@@ -190,26 +198,32 @@ class Buffer {
     eraseLineToCursor();
 
     for (var i = 0; i < _cursorY; i++) {
-      getViewLine(i).erase(terminal.cursor, 0, terminal.viewWidth);
+      final line = getViewLine(i);
+      line.isWrapped = false;
+      line.erase(terminal.cursor, 0, terminal.viewWidth);
     }
   }
 
   void eraseDisplay() {
     for (var i = 0; i < terminal.viewHeight; i++) {
       final line = getViewLine(i);
+      line.isWrapped = false;
       line.erase(terminal.cursor, 0, terminal.viewWidth);
     }
   }
 
   void eraseLineFromCursor() {
+    currentLine.isWrapped = false;
     currentLine.erase(terminal.cursor, _cursorX, terminal.viewWidth);
   }
 
   void eraseLineToCursor() {
+    currentLine.isWrapped = false;
     currentLine.erase(terminal.cursor, 0, _cursorX);
   }
 
   void eraseLine() {
+    currentLine.isWrapped = false;
     currentLine.erase(terminal.cursor, 0, terminal.viewWidth);
   }
 
@@ -408,7 +422,7 @@ class Buffer {
   void deleteChars(int count) {
     final start = _cursorX.clamp(0, terminal.viewWidth);
     final end = min(_cursorX + count, terminal.viewWidth);
-    currentLine.removeRange(start, end);
+    currentLine.clearRange(start, end);
   }
 
   void clearScrollback() {
@@ -478,17 +492,18 @@ class Buffer {
     lines.remove(index);
   }
 
-  void resize(int newWidth, int newHeight) {
-    if (newWidth > terminal.viewWidth) {
-      lines.forEach((line) {
-        line.ensure(newWidth);
-      });
+  void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
+    if (newWidth > oldWidth) {
+      lines.forEach((item) => item.ensure(newWidth));
     }
 
-    if (newHeight > terminal.viewHeight) {
+    if (newHeight > oldHeight) {
+      while (lines.length < newHeight) {
+        lines.push(_newEmptyLine());
+      }
       // Grow larger
-      for (var i = 0; i < newHeight - terminal.viewHeight; i++) {
-        if (_cursorY < terminal.viewHeight - 1) {
+      for (var i = 0; i < newHeight - oldHeight; i++) {
+        if (_cursorY < oldHeight - 1) {
           lines.push(_newEmptyLine());
         } else {
           _cursorY++;
@@ -496,8 +511,8 @@ class Buffer {
       }
     } else {
       // Shrink smaller
-      for (var i = 0; i < terminal.viewHeight - newHeight; i++) {
-        if (_cursorY < terminal.viewHeight - 1) {
+      for (var i = 0; i < oldHeight - newHeight; i++) {
+        if (_cursorY < oldHeight - 1) {
           lines.pop();
         } else {
           _cursorY++;
@@ -508,11 +523,26 @@ class Buffer {
     // Ensure cursor is within the screen.
     _cursorX = _cursorX.clamp(0, newWidth - 1);
     _cursorY = _cursorY.clamp(0, newHeight - 1);
+
+    if (!terminal.isUsingAltBuffer()) {
+      final reflowStrategy = newWidth > oldWidth
+          ? ReflowStrategyWider(this)
+          : ReflowStrategyNarrower(this);
+      reflowStrategy.reflow(newWidth, newHeight, oldWidth, oldHeight);
+    }
   }
 
   BufferLine _newEmptyLine() {
-    final line = BufferLine();
-    line.ensure(terminal.viewWidth);
+    final line = BufferLine(length: terminal.viewWidth);
     return line;
   }
+
+  adjustSavedCursor(int dx, int dy) {
+    if (_savedCursorX != null) {
+      _savedCursorX = _savedCursorX! + dx;
+    }
+    if (_savedCursorY != null) {
+      _savedCursorY = _savedCursorY! + dy;
+    }
+  }
 }

+ 143 - 28
lib/buffer/buffer_line.dart

@@ -1,3 +1,4 @@
+import 'dart:math';
 import 'dart:typed_data';
 
 import 'package:xterm/terminal/cursor.dart';
@@ -15,6 +16,7 @@ import 'package:xterm/terminal/cursor.dart';
 ///      1byte     1byte     1byte      1byte
 
 const _cellSize = 16;
+const _cellSize64Bit = _cellSize >> 3;
 
 const _cellContent = 0;
 const _cellFgColor = 4;
@@ -24,75 +26,106 @@ const _cellBgColor = 8;
 const _cellWidth = 12;
 const _cellFlags = 13;
 
+int _nextLength(int lengthRequirement) {
+  var nextLength = 2;
+  while (nextLength < lengthRequirement) {
+    nextLength *= 2;
+  }
+  return nextLength;
+}
+
 class BufferLine {
-  BufferLine() {
-    const initLength = 64;
-    _cells = ByteData(initLength * _cellSize);
+  BufferLine({int length = 64, this.isWrapped = false}) {
+    _maxCols = _nextLength(length);
+    _cells = ByteData(_maxCols * _cellSize);
   }
 
   late ByteData _cells;
 
-  bool get isWrapped => _isWrapped;
-  bool _isWrapped = false;
+  bool isWrapped;
 
-  void ensure(int length) {
-    final expectedLengthInBytes = length * _cellSize;
+  int _maxCols = 64;
 
-    if (expectedLengthInBytes < _cells.lengthInBytes) {
+  void ensure(int length) {
+    if (length <= _maxCols) {
       return;
     }
 
-    var newLengthInBytes = _cells.lengthInBytes;
-    while (newLengthInBytes < expectedLengthInBytes) {
-      newLengthInBytes *= 2;
-    }
-
-    final newCells = ByteData(newLengthInBytes);
+    final nextLength = _nextLength(length);
+    final newCells = ByteData(nextLength * _cellSize);
     newCells.buffer.asInt64List().setAll(0, _cells.buffer.asInt64List());
     _cells = newCells;
+    _maxCols = nextLength;
   }
 
   void insert(int index) {
     insertN(index, 1);
   }
 
+  void removeN(int index, int count) {
+    final moveStart = index * _cellSize64Bit;
+    final moveOffset = count * _cellSize64Bit;
+    final moveEnd = (_maxCols - count) * _cellSize64Bit;
+    final bufferEnd = _maxCols * _cellSize64Bit;
+
+    // move data backward
+    final cells = _cells.buffer.asInt64List();
+    for (var i = moveStart; i < moveEnd; i++) {
+      cells[i] = cells[i + moveOffset];
+    }
+
+    // set empty cells to 0
+    for (var i = moveEnd; i < bufferEnd; i++) {
+      cells[i] = 0x00;
+    }
+  }
+
   void insertN(int index, int count) {
     //                       start
     // +--------------------------|-----------------------------------+
     // |                          |                                   |
-    // +--------------------------\--\--------------------------------+
+    // +--------------------------\--\--------------------------------+ end
     //                             \  \
     //                              \  \
     //                               v  v
     // +--------------------------|--|--------------------------------+
     // |                          |  |                                |
-    // +--------------------------|--|--------------------------------+
+    // +--------------------------|--|--------------------------------+ end
     //                       start   start+offset
 
-    final start = (index * _cellSize).clamp(0, _cells.lengthInBytes);
-    final offset = (count * _cellSize).clamp(0, _cells.lengthInBytes - start);
+    final moveStart = index * _cellSize64Bit;
+    final moveOffset = count * _cellSize64Bit;
+    final bufferEnd = _maxCols * _cellSize64Bit;
 
     // move data forward
-    final cells = _cells.buffer.asInt8List();
-    for (var i = _cells.lengthInBytes - offset - 1; i >= start; i++) {
-      cells[i + offset] = cells[i];
+    final cells = _cells.buffer.asInt64List();
+    for (var i = bufferEnd - moveOffset - 1; i >= moveStart; i--) {
+      cells[i + moveOffset] = cells[i];
     }
 
     // set inserted cells to 0
-    for (var i = start; i < start + offset; i++) {
+    for (var i = moveStart; i < moveStart + moveOffset; i++) {
       cells[i] = 0x00;
     }
   }
 
   void clear() {
-    removeRange(0, _cells.lengthInBytes ~/ _cellSize);
+    clearRange(0, _cells.lengthInBytes ~/ _cellSize);
   }
 
-  void erase(Cursor cursor, int start, int end) {
+  void erase(Cursor cursor, int start, int end, [bool resetIsWrapped = false]) {
     ensure(end);
     for (var i = start; i < end; i++) {
       cellErase(i, cursor);
     }
+    if (resetIsWrapped) {
+      isWrapped = false;
+    }
+  }
+
+  void cellClear(int index) {
+    _cells.setInt64(index * _cellSize, 0x00);
+    _cells.setInt64(index * _cellSize + 8, 0x00);
   }
 
   void cellInitialize(
@@ -109,15 +142,22 @@ class BufferLine {
     _cells.setInt8(cell + _cellFlags, cursor.flags);
   }
 
+  bool cellHasContent(int index) {
+    return cellGetContent(index) != 0;
+  }
+
   int cellGetContent(int index) {
-    return _cells.getInt32(index * _cellSize + _cellContent);
+    return _cells.getUint32(index * _cellSize + _cellContent);
   }
 
   void cellSetContent(int index, int content) {
-    return _cells.setInt32(index * _cellSize + _cellContent, content);
+    _cells.setInt32(index * _cellSize + _cellContent, content);
   }
 
   int cellGetFgColor(int index) {
+    if (index >= _maxCols) {
+      return 0;
+    }
     return _cells.getInt32(index * _cellSize + _cellFgColor);
   }
 
@@ -126,6 +166,9 @@ class BufferLine {
   }
 
   int cellGetBgColor(int index) {
+    if (index >= _maxCols) {
+      return 0;
+    }
     return _cells.getInt32(index * _cellSize + _cellBgColor);
   }
 
@@ -134,6 +177,9 @@ class BufferLine {
   }
 
   int cellGetFlags(int index) {
+    if (index >= _maxCols) {
+      return 0;
+    }
     return _cells.getInt8(index * _cellSize + _cellFlags);
   }
 
@@ -142,6 +188,9 @@ class BufferLine {
   }
 
   int cellGetWidth(int index) {
+    if (index >= _maxCols) {
+      return 1;
+    }
     return _cells.getInt8(index * _cellSize + _cellWidth);
   }
 
@@ -154,6 +203,9 @@ class BufferLine {
   }
 
   bool cellHasFlag(int index, int flag) {
+    if (index >= _maxCols) {
+      return false;
+    }
     return cellGetFlags(index) & flag != 0;
   }
 
@@ -166,6 +218,38 @@ class BufferLine {
     cellSetFgColor(index, cursor.fg);
     cellSetBgColor(index, cursor.bg);
     cellSetFlags(index, cursor.flags);
+    cellSetWidth(index, 0);
+  }
+
+  int getTrimmedLength([int? cols]) {
+    if (cols == null) {
+      cols = _maxCols;
+    }
+    for (var i = cols - 1; i >= 0; i--) {
+      if (cellGetContent(i) != 0) {
+        // we are at the last cell in this line that has content.
+        // the length of this line is the index of this cell + 1
+        // the only exception is that if that last cell is wider
+        // than 1 then we have to add the diff
+        final lastCellWidth = cellGetWidth(i);
+        return i + lastCellWidth;
+      }
+    }
+    return 0;
+  }
+
+  void copyCellsFrom(BufferLine src, int srcCol, int dstCol, int len) {
+    ensure(dstCol + len);
+
+    final intsToCopy = len * _cellSize64Bit;
+    final srcStart = srcCol * _cellSize64Bit;
+    final dstStart = dstCol * _cellSize64Bit;
+
+    final cells = _cells.buffer.asInt64List();
+    final srcCells = src._cells.buffer.asInt64List();
+    for (var i = 0; i < intsToCopy; i++) {
+      cells[dstStart + i] = srcCells[srcStart + i];
+    }
   }
 
   // int cellGetHash(int index) {
@@ -175,13 +259,44 @@ class BufferLine {
   //   return a ^ b;
   // }
 
-  void removeRange(int start, int end) {
+  void clearRange(int start, int end) {
+    end = min(end, _maxCols);
     // start = start.clamp(0, _cells.length);
     // end ??= _cells.length;
     // end = end.clamp(start, _cells.length);
     // _cells.removeRange(start, end);
     for (var index = start; index < end; index++) {
-      cellSetContent(index, 0x00);
+      cellClear(index);
+    }
+  }
+
+  @override
+  String toString() {
+    final result = StringBuffer();
+    for (int i = 0; i < _maxCols; i++) {
+      final code = cellGetContent(i);
+      if (code == 0) {
+        continue;
+      }
+      result.writeCharCode(code);
+    }
+    return result.toString();
+  }
+
+  String toDebugString(int cols) {
+    final result = StringBuffer();
+    final length = getTrimmedLength();
+    for (int i = 0; i < max(cols, length); i++) {
+      var code = cellGetContent(i);
+      if (code == 0) {
+        if (cellGetWidth(i) == 0) {
+          code = '_'.runes.first;
+        } else {
+          code = cellGetWidth(i).toString().runes.first;
+        }
+      }
+      result.writeCharCode(code);
     }
+    return result.toString();
   }
 }

+ 13 - 0
lib/buffer/reflow_strategy.dart

@@ -0,0 +1,13 @@
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/utli/circular_list.dart';
+
+abstract class ReflowStrategy {
+  final Buffer _buffer;
+
+  ReflowStrategy(this._buffer);
+
+  Buffer get buffer => _buffer;
+
+  void reflow(int newCols, int newRows, int oldCols, int oldRows);
+}

+ 93 - 0
lib/buffer/reflow_strategy_narrower.dart

@@ -0,0 +1,93 @@
+import 'dart:math';
+
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/reflow_strategy.dart';
+
+class ReflowStrategyNarrower extends ReflowStrategy {
+  ReflowStrategyNarrower(Buffer buffer) : super(buffer);
+
+  @override
+  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
+    final linesAfterReflow = <BufferLine>[];
+
+    //print('Reflow narrower $oldCols -> $newCols');
+    for (var i = 0; i < buffer.lines.length; i++) {
+      final line = buffer.lines[i];
+      final lineLength = line.getTrimmedLength();
+      linesAfterReflow.add(line);
+
+      if (lineLength > newCols) {
+        var moveIndexStart = newCols;
+        var cellsToCopy = lineLength - newCols;
+
+        // when we have a double width character and are about to move the "0" placeholder,
+        // then we have to move the double width character as well
+        if (line.cellGetContent(moveIndexStart) == 0 &&
+            line.cellGetWidth(moveIndexStart - 1) == 2) {
+          moveIndexStart -= 1;
+          cellsToCopy += 1;
+        }
+
+        var addZero = false;
+        //when the last cell to copy is a double width cell, then add a "0"
+        if (line.cellGetWidth(moveIndexStart + cellsToCopy - 1) == 2) {
+          addZero = true;
+        }
+
+        // var alreadyInserted = 0;
+
+        //when we have aggregated a whole new line then insert it now
+        while (cellsToCopy > newCols) {
+          final newLine = BufferLine(length: newCols, isWrapped: true);
+          newLine.copyCellsFrom(line, moveIndexStart, 0, newCols);
+          // line.clearRange(moveIndexStart, moveIndexStart + newCols);
+          line.removeN(moveIndexStart, newCols);
+
+          linesAfterReflow.add(newLine);
+
+          cellsToCopy -= newCols;
+          // alreadyInserted++;
+        }
+
+        // we need to move cut cells to the next line
+        // if the next line is wrapped anyway, we can push them onto the beginning of that line
+        // otherwise, we need add a new wrapped line
+        // final nextLineIndex = i + alreadyInserted + 1;
+        final nextLineIndex = i + 1;
+        if (nextLineIndex < buffer.lines.length) {
+          final nextLine = buffer.lines[nextLineIndex];
+          if (nextLine.isWrapped) {
+            final nextLineLength = nextLine.getTrimmedLength();
+            nextLine.ensure(nextLineLength + cellsToCopy + (addZero ? 1 : 0));
+            nextLine.insertN(0, cellsToCopy + (addZero ? 1 : 0));
+            nextLine.copyCellsFrom(line, moveIndexStart, 0, cellsToCopy);
+            // clean the cells that we moved
+            line.removeN(moveIndexStart, cellsToCopy);
+            // line.erase(buffer.terminal.cursor, moveIndexStart,
+            //     moveIndexStart + cellsToCopy);
+            //print('M: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
+            //print(
+            //    'N: ${i + 1 < 10 ? '0' : ''}${i + 1}: ${nextLine.toDebugString(oldCols)}');
+            continue;
+          }
+        }
+
+        final newLine = BufferLine(length: newCols, isWrapped: true);
+        newLine.copyCellsFrom(line, moveIndexStart, 0, cellsToCopy);
+        // clean the cells that we moved
+        line.removeN(moveIndexStart, cellsToCopy);
+
+        linesAfterReflow.add(newLine);
+
+        //TODO: scrolling is a bit weird afterwards
+
+        //print('S: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
+      } else {
+        //print('N: ${i < 10 ? '0' : ''}$i: ${line.toDebugString(oldCols)}');
+      }
+    }
+
+    buffer.lines.replaceWith(linesAfterReflow);
+  }
+}

+ 67 - 0
lib/buffer/reflow_strategy_wider.dart

@@ -0,0 +1,67 @@
+import 'dart:math';
+
+import 'package:xterm/buffer/buffer.dart';
+import 'package:xterm/buffer/buffer_line.dart';
+import 'package:xterm/buffer/reflow_strategy.dart';
+
+class ReflowStrategyWider extends ReflowStrategy {
+  ReflowStrategyWider(Buffer buffer) : super(buffer);
+
+  @override
+  void reflow(int newCols, int newRows, int oldCols, int oldRows) {
+    for (var i = 0; i < buffer.lines.length; i++) {
+      final line = buffer.lines[i];
+      line.ensure(newCols);
+      for (var offset = 1; i + offset < buffer.lines.length; offset++) {
+        final nextLine = buffer.lines[i + offset];
+        if (!nextLine.isWrapped) {
+          break;
+        }
+        // when we are reflowing wider we can be sure that this line and the next all have equal to or less than
+        // 'newCols' length => we can pass newCols as the upper limit
+        final lineLength = line.getTrimmedLength(newCols);
+
+        var copyDestIndex = lineLength;
+        if (copyDestIndex >= 1 &&
+            line.cellGetWidth(copyDestIndex - 1) == 2 &&
+            line.cellGetContent(copyDestIndex) == 0) {
+          //we would override a wide char placeholder => move index one to the right
+          copyDestIndex += 1;
+        }
+
+        final spaceOnLine = newCols - copyDestIndex;
+        if (spaceOnLine <= 0) {
+          // no more space to unwrap
+          break;
+        }
+        // when we are reflowing wider we can be sure that this line and the next all have equal to or less than
+        // 'newCols' length => we can pass newCols as the upper limit
+        final nextLineLength = nextLine.getTrimmedLength(newCols);
+        var moveCount = min(spaceOnLine, nextLineLength);
+        if (moveCount <= 0) {
+          break;
+        }
+
+        // when we are about to copy a double width character
+        // to the end of the line then we just ignore it as the target width
+        // would be too much
+        if (nextLine.cellGetWidth(moveCount - 1) == 2) {
+          moveCount -= 1;
+        }
+        line.copyCellsFrom(nextLine, 0, copyDestIndex, moveCount);
+        if (moveCount >= nextLineLength) {
+          // if we unwrapped all cells off the next line, delete it
+          buffer.lines.remove(i + offset);
+          offset--;
+        } else {
+          // otherwise just remove the characters we moved up a line
+          nextLine.removeN(0, moveCount);
+        }
+      }
+    }
+    //buffer doesn't have enough lines
+    while (buffer.lines.length < buffer.terminal.viewHeight) {
+      buffer.lines.push(BufferLine());
+    }
+  }
+}

+ 45 - 22
lib/frontend/terminal_view.dart

@@ -104,14 +104,12 @@ class _TerminalViewState extends State<TerminalView> {
   double? _terminalScrollExtent;
 
   void onTerminalChange() {
-    if (!mounted) {
-      return;
-    }
-
     _terminalScrollExtent =
         _cellSize.cellHeight * widget.terminal.buffer.scrollOffsetFromTop;
 
-    setState(() {});
+    if (mounted) {
+      setState(() {});
+    }
   }
 
   // listen to oscillator to update mouse blink etc.
@@ -165,14 +163,20 @@ class _TerminalViewState extends State<TerminalView> {
           onSize(constraints.maxWidth, constraints.maxHeight);
           // use flutter's Scrollable to manage scrolling to better integrate
           // with widgets such as Scrollbar.
-          return NotificationListener<UserScrollNotification>(
-            onNotification: (_) {
-              onScroll(_.metrics.pixels);
+          return NotificationListener<ScrollNotification>(
+            onNotification: (notification) {
+              onScroll(notification.metrics.pixels);
               return false;
             },
             child: Scrollable(
               controller: widget.scrollController,
               viewportBuilder: (context, offset) {
+                /// use [_EmptyScrollActivity] to suppress unexpected behaviors
+                /// that come from [applyViewportDimension].
+                widget.scrollController.position.beginActivity(
+                  _EmptyScrollActivity(),
+                );
+
                 // set viewport height.
                 offset.applyViewportDimension(constraints.maxHeight);
 
@@ -269,22 +273,15 @@ class _TerminalViewState extends State<TerminalView> {
     final termWidth = (width / _cellSize.cellWidth).floor();
     final termHeight = (height / _cellSize.cellHeight).floor();
 
-    if (_lastTerminalWidth != termWidth || _lastTerminalHeight != termHeight) {
-      _lastTerminalWidth = termWidth;
-      _lastTerminalHeight = termHeight;
-
-      // print('($termWidth, $termHeight)');
-
-      widget.onResize?.call(termWidth, termHeight);
+    if (_lastTerminalWidth == termWidth && _lastTerminalHeight == termHeight) {
+      return;
+    }
 
-      SchedulerBinding.instance!.addPostFrameCallback((_) {
-        widget.terminal.resize(termWidth, termHeight);
-      });
+    _lastTerminalWidth = termWidth;
+    _lastTerminalHeight = termHeight;
 
-      // Future.delayed(Duration.zero).then((_) {
-      //   widget.terminal.resize(termWidth, termHeight);
-      // });
-    }
+    widget.onResize?.call(termWidth, termHeight);
+    widget.terminal.resize(termWidth, termHeight);
   }
 
   TextEditingValue? onInput(TextEditingValue value) {
@@ -292,6 +289,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);
   }
@@ -552,3 +550,28 @@ class TerminalPainter extends CustomPainter {
     return terminal.dirty;
   }
 }
+
+/// A scroll activity delegate that does nothing. Used to construct
+/// [_EmptyScrollActivity].
+class _EmptyScrollActivityDelegate implements ScrollActivityDelegate {
+  const _EmptyScrollActivityDelegate();
+
+  final axisDirection = AxisDirection.down;
+
+  double setPixels(double pixels) => 0;
+
+  void applyUserOffset(double delta) {}
+
+  void goIdle() {}
+
+  void goBallistic(double velocity) {}
+}
+
+/// A scroll activity that does nothing. Used to suppress unexpected behaviors
+/// from [Scrollable].
+class _EmptyScrollActivity extends IdleScrollActivity {
+  _EmptyScrollActivity() : super(_EmptyScrollActivityDelegate());
+
+  @override
+  void applyNewDimensions() {}
+}

+ 6 - 4
lib/terminal/terminal.dart

@@ -90,6 +90,7 @@ class Terminal with Observable {
 
   bool _screenMode = false; // DECSCNM (black on white background)
   bool _autoWrapMode = true;
+  bool get autoWrapMode => _autoWrapMode;
 
   /// ### DECOM – Origin Mode (DEC Private)
   ///
@@ -342,13 +343,14 @@ class Terminal with Observable {
     newWidth = max(newWidth, 1);
     newHeight = max(newHeight, 1);
 
-    buffer.resize(newWidth, newHeight);
-
-    // maybe reflow should happen here.
-
+    final oldWidth = _viewWidth;
+    final oldHeight = _viewHeight;
     _viewWidth = newWidth;
     _viewHeight = newHeight;
 
+    buffer.resize(oldWidth, oldHeight, newWidth, newHeight);
+
+    // maybe reflow should happen here.
     if (buffer == _altBuffer) {
       buffer.clearScrollback();
     }

+ 21 - 0
lib/utli/circular_list.dart

@@ -74,6 +74,12 @@ class CircularList<T> {
     _length = 0;
   }
 
+  void pushAll(Iterable<T> items) {
+    items.forEach((element) {
+      push(element);
+    });
+  }
+
   void push(T value) {
     _array[_getCyclicIndex(_length)] = value;
     if (_length == _array.length) {
@@ -167,6 +173,21 @@ class CircularList<T> {
     }
   }
 
+  void replaceWith(List<T> replacement) {
+    var copyStart = 0;
+    if (replacement.length > maxLength) {
+      copyStart = replacement.length - maxLength;
+    }
+
+    final copyLength = replacement.length - copyStart;
+    for (var i = 0; i < copyLength; i++) {
+      _array[i] = replacement[copyStart + i];
+    }
+
+    _startIndex = 0;
+    _length = copyLength;
+  }
+
   bool get isFull => length == maxLength;
 
   List<T> toList() {