| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604 |
- import 'dart:math' show max, min;
- import 'package:xterm/src/core/buffer/cell_offset.dart';
- import 'package:xterm/src/core/buffer/line.dart';
- import 'package:xterm/src/core/buffer/range_line.dart';
- import 'package:xterm/src/core/buffer/range.dart';
- import 'package:xterm/src/core/charset.dart';
- import 'package:xterm/src/core/cursor.dart';
- import 'package:xterm/src/core/reflow.dart';
- import 'package:xterm/src/core/state.dart';
- import 'package:xterm/src/utils/circular_buffer.dart';
- import 'package:xterm/src/utils/unicode_v11.dart';
- class Buffer {
- final TerminalState terminal;
- final int maxLines;
- final bool isAltBuffer;
- /// Characters that break selection when calling [getWordBoundary]. If null,
- /// defaults to [defaultWordSeparators].
- final Set<int>? wordSeparators;
- Buffer(
- this.terminal, {
- required this.maxLines,
- required this.isAltBuffer,
- this.wordSeparators,
- }) {
- for (int i = 0; i < terminal.viewHeight; i++) {
- lines.push(_newEmptyLine());
- }
- resetVerticalMargins();
- }
- int _cursorX = 0;
- int _cursorY = 0;
- late int _marginTop;
- late int _marginBottom;
- var _savedCursorX = 0;
- var _savedCursorY = 0;
- final _savedCursorStyle = CursorStyle();
- final charset = Charset();
- /// Width of the viewport in columns. Also the index of the last column.
- int get viewWidth => terminal.viewWidth;
- /// Height of the viewport in rows. Also the index of the last line.
- int get viewHeight => terminal.viewHeight;
- /// lines of the buffer. the length of [lines] should always be equal or
- /// greater than [viewHeight].
- late final lines = IndexAwareCircularBuffer<BufferLine>(maxLines);
- /// Total number of lines in the buffer. Always equal or greater than
- /// [viewHeight].
- int get height => lines.length;
- /// Horizontal position of the cursor relative to the top-left cornor of the
- /// screen, starting from 0.
- int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
- /// Vertical position of the cursor relative to the top-left cornor of the
- /// screen, starting from 0.
- int get cursorY => _cursorY;
- /// Index of the first line in the scroll region.
- int get marginTop => _marginTop;
- /// Index of the last line in the scroll region.
- int get marginBottom => _marginBottom;
- /// The number of lines above the viewport.
- int get scrollBack => height - viewHeight;
- /// Vertical position of the cursor relative to the top of the buffer,
- /// starting from 0.
- int get absoluteCursorY => _cursorY + scrollBack;
- /// Absolute index of the first line in the scroll region.
- int get absoluteMarginTop => _marginTop + scrollBack;
- /// Absolute index of the last line in the scroll region.
- int get absoluteMarginBottom => _marginBottom + scrollBack;
- /// Writes data to the _terminal. Terminal sequences or special characters are
- /// not interpreted and directly added to the buffer.
- ///
- /// See also: [Terminal.write]
- void write(String text) {
- for (var char in text.runes) {
- writeChar(char);
- }
- }
- /// Writes a single character to the _terminal. Escape sequences or special
- /// characters are not interpreted and directly added to the buffer.
- ///
- /// See also: [Terminal.writeChar]
- void writeChar(int codePoint) {
- codePoint = charset.translate(codePoint);
- final cellWidth = unicodeV11.wcwidth(codePoint);
- if (_cursorX >= terminal.viewWidth) {
- index();
- setCursorX(0);
- if (terminal.autoWrapMode) {
- currentLine.isWrapped = true;
- }
- }
- final line = currentLine;
- line.setCell(_cursorX, codePoint, cellWidth, terminal.cursor);
- if (_cursorX < viewWidth) {
- _cursorX++;
- }
- if (cellWidth == 2) {
- writeChar(0);
- }
- }
- /// The line at the current cursor position.
- BufferLine get currentLine {
- return lines[absoluteCursorY];
- }
- void backspace() {
- if (_cursorX == 0 && currentLine.isWrapped) {
- currentLine.isWrapped = false;
- moveCursor(viewWidth - 1, -1);
- } else if (_cursorX == viewWidth) {
- moveCursor(-2, 0);
- } else {
- moveCursor(-1, 0);
- }
- }
- /// Erases the viewport from the cursor position to the end of the buffer,
- /// including the cursor position.
- void eraseDisplayFromCursor() {
- eraseLineFromCursor();
- for (var i = absoluteCursorY + 1; i < height; i++) {
- final line = lines[i];
- line.isWrapped = false;
- line.eraseRange(0, viewWidth, terminal.cursor);
- }
- }
- /// Erases the viewport from the top-left corner to the cursor, including the
- /// cursor.
- void eraseDisplayToCursor() {
- eraseLineToCursor();
- for (var i = 0; i < _cursorY; i++) {
- final line = lines[i + scrollBack];
- line.isWrapped = false;
- line.eraseRange(0, viewWidth, terminal.cursor);
- }
- }
- /// Erases the whole viewport.
- void eraseDisplay() {
- for (var i = 0; i < viewHeight; i++) {
- final line = lines[i + scrollBack];
- line.isWrapped = false;
- line.eraseRange(0, viewWidth, terminal.cursor);
- }
- }
- /// Erases the line from the cursor to the end of the line, including the
- /// cursor position.
- void eraseLineFromCursor() {
- currentLine.isWrapped = false;
- currentLine.eraseRange(_cursorX, viewWidth, terminal.cursor);
- }
- /// Erases the line from the start of the line to the cursor, including the
- /// cursor.
- void eraseLineToCursor() {
- currentLine.isWrapped = false;
- currentLine.eraseRange(0, _cursorX, terminal.cursor);
- }
- /// Erases the line at the current cursor position.
- void eraseLine() {
- currentLine.isWrapped = false;
- currentLine.eraseRange(0, viewWidth, terminal.cursor);
- }
- /// Erases [count] cells starting at the cursor position.
- void eraseChars(int count) {
- final start = _cursorX;
- currentLine.eraseRange(start, start + count, terminal.cursor);
- }
- void scrollDown(int lines) {
- for (var i = absoluteMarginBottom; i >= absoluteMarginTop; i--) {
- if (i >= absoluteMarginTop + lines) {
- this.lines[i] = this.lines[i - lines];
- } else {
- this.lines[i] = _newEmptyLine();
- }
- }
- }
- void scrollUp(int lines) {
- for (var i = absoluteMarginTop; i <= absoluteMarginBottom; i++) {
- if (i <= absoluteMarginBottom - lines) {
- this.lines[i] = this.lines[i + lines];
- } else {
- this.lines[i] = _newEmptyLine();
- }
- }
- }
- /// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND – Index
- ///
- /// ESC D
- ///
- /// [index] causes the active position to move downward one line without
- /// changing the column position. If the active position is at the bottom
- /// margin, a scroll up is performed.
- void index() {
- if (isInVerticalMargin) {
- if (_cursorY == _marginBottom) {
- if (marginTop == 0 && !isAltBuffer) {
- lines.insert(absoluteMarginBottom + 1, _newEmptyLine());
- } else {
- scrollUp(1);
- }
- } else {
- moveCursorY(1);
- }
- return;
- }
- // the cursor is not in the scrollable region
- if (_cursorY >= viewHeight - 1) {
- // we are at the bottom
- if (isAltBuffer) {
- scrollUp(1);
- } else {
- lines.push(_newEmptyLine());
- }
- } else {
- // there're still lines so we simply move cursor down.
- moveCursorY(1);
- }
- }
- void lineFeed() {
- index();
- if (terminal.lineFeedMode) {
- setCursorX(0);
- }
- }
- /// https://terminalguide.namepad.de/seq/a_esc_cm/
- void reverseIndex() {
- if (isInVerticalMargin) {
- if (_cursorY == _marginTop) {
- scrollDown(1);
- } else {
- moveCursorY(-1);
- }
- } else {
- moveCursorY(-1);
- }
- }
- void cursorGoForward() {
- _cursorX = min(_cursorX + 1, viewWidth);
- }
- void setCursorX(int cursorX) {
- _cursorX = cursorX.clamp(0, viewWidth - 1);
- }
- void setCursorY(int cursorY) {
- _cursorY = cursorY.clamp(0, viewHeight - 1);
- }
- void moveCursorX(int offset) {
- setCursorX(_cursorX + offset);
- }
- void moveCursorY(int offset) {
- setCursorY(_cursorY + offset);
- }
- void setCursor(int cursorX, int cursorY) {
- var maxCursorY = viewHeight - 1;
- if (terminal.originMode) {
- cursorY += _marginTop;
- maxCursorY = _marginBottom;
- }
- _cursorX = cursorX.clamp(0, viewWidth - 1);
- _cursorY = cursorY.clamp(0, maxCursorY);
- }
- void moveCursor(int offsetX, int offsetY) {
- final cursorX = _cursorX + offsetX;
- final cursorY = _cursorY + offsetY;
- setCursor(cursorX, cursorY);
- }
- /// Save cursor position, charmap and text attributes.
- void saveCursor() {
- _savedCursorX = _cursorX;
- _savedCursorY = _cursorY;
- _savedCursorStyle.foreground = terminal.cursor.foreground;
- _savedCursorStyle.background = terminal.cursor.background;
- _savedCursorStyle.attrs = terminal.cursor.attrs;
- charset.save();
- }
- /// Restore cursor position, charmap and text attributes.
- void restoreCursor() {
- _cursorX = _savedCursorX;
- _cursorY = _savedCursorY;
- terminal.cursor.foreground = _savedCursorStyle.foreground;
- terminal.cursor.background = _savedCursorStyle.background;
- terminal.cursor.attrs = _savedCursorStyle.attrs;
- charset.restore();
- }
- /// Sets the vertical scrolling margin to [top] and [bottom].
- /// Both values must be between 0 and [viewHeight] - 1.
- void setVerticalMargins(int top, int bottom) {
- _marginTop = top.clamp(0, viewHeight - 1);
- _marginBottom = bottom.clamp(0, viewHeight - 1);
- _marginTop = min(_marginTop, _marginBottom);
- _marginBottom = max(_marginTop, _marginBottom);
- }
- bool get isInVerticalMargin {
- return _cursorY >= _marginTop && _cursorY <= _marginBottom;
- }
- void resetVerticalMargins() {
- setVerticalMargins(0, viewHeight - 1);
- }
- void deleteChars(int count) {
- final start = _cursorX.clamp(0, viewWidth);
- count = min(count, viewWidth - start);
- currentLine.removeCells(start, count, terminal.cursor);
- }
- /// Remove all lines above the top of the viewport.
- void clearScrollback() {
- if (height <= viewHeight) {
- return;
- }
- lines.trimStart(scrollBack);
- }
- /// Clears the viewport and scrollback buffer. Then fill with empty lines.
- void clear() {
- lines.clear();
- for (int i = 0; i < viewHeight; i++) {
- lines.push(_newEmptyLine());
- }
- }
- void insertBlankChars(int count) {
- currentLine.insertCells(_cursorX, count, terminal.cursor);
- }
- void insertLines(int count) {
- if (!isInVerticalMargin) {
- return;
- }
- setCursorX(0);
- // Number of lines from the cursor to the bottom of the scrollable region
- // including the cursor itself.
- final linesBelow = absoluteMarginBottom - absoluteCursorY + 1;
- // Number of empty lines to insert.
- final linesToInsert = min(count, linesBelow);
- // Number of lines to move up.
- final linesToMove = linesBelow - linesToInsert;
- for (var i = 0; i < linesToMove; i++) {
- final index = absoluteMarginBottom - i;
- lines[index] = lines.swap(index - linesToInsert, _newEmptyLine());
- }
- for (var i = linesToMove; i < linesToInsert; i++) {
- lines[absoluteCursorY + i] = _newEmptyLine();
- }
- }
- /// Remove [count] lines starting at the current cursor position. Lines below
- /// the removed lines are shifted up. This only affects the scrollable region.
- /// Lines outside the scrollable region are not affected.
- void deleteLines(int count) {
- if (!isInVerticalMargin) {
- return;
- }
- setCursorX(0);
- count = min(count, absoluteMarginBottom - absoluteCursorY + 1);
- final linesToMove = absoluteMarginBottom - absoluteCursorY + 1 - count;
- for (var i = 0; i < linesToMove; i++) {
- final index = absoluteCursorY + i;
- lines[index] = lines[index + count];
- }
- for (var i = 0; i < count; i++) {
- lines[absoluteMarginBottom - i] = _newEmptyLine();
- }
- }
- void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
- // 1. Adjust the height.
- if (newHeight > oldHeight) {
- // Grow larger
- for (var i = 0; i < newHeight - oldHeight; i++) {
- if (newHeight > lines.length) {
- lines.push(_newEmptyLine(newWidth));
- } else {
- _cursorY++;
- }
- }
- } else {
- // Shrink smaller
- for (var i = 0; i < oldHeight - newHeight; i++) {
- if (_cursorY > newHeight - 1) {
- _cursorY--;
- } else {
- lines.pop();
- }
- }
- }
- // Ensure cursor is within the screen.
- _cursorX = _cursorX.clamp(0, newWidth - 1);
- _cursorY = _cursorY.clamp(0, newHeight - 1);
- // 2. Adjust the width.
- if (newWidth != oldWidth) {
- if (terminal.reflowEnabled && !isAltBuffer) {
- final reflowResult = reflow(lines, oldWidth, newWidth);
- while (reflowResult.length < newHeight) {
- reflowResult.add(_newEmptyLine(newWidth));
- }
- lines.replaceWith(reflowResult);
- } else {
- lines.forEach((item) => item.resize(newWidth));
- }
- }
- }
- /// Create a new [CellAnchor] at the specified [x] and [y] coordinates.
- CellAnchor createAnchor(int x, int y) {
- return lines[y].createAnchor(x);
- }
- /// Create a new [CellAnchor] at the specified [x] and [y] coordinates.
- CellAnchor createAnchorFromOffset(CellOffset offset) {
- return lines[offset.y].createAnchor(offset.x);
- }
- CellAnchor createAnchorFromCursor() {
- return createAnchor(cursorX, absoluteCursorY);
- }
- /// Create a new empty [BufferLine] with the current [viewWidth] if [width]
- /// is not specified.
- BufferLine _newEmptyLine([int? width]) {
- final line = BufferLine(width ?? viewWidth);
- return line;
- }
- static final defaultWordSeparators = <int>{
- 0,
- r' '.codeUnitAt(0),
- r'.'.codeUnitAt(0),
- r':'.codeUnitAt(0),
- r'-'.codeUnitAt(0),
- r'\'.codeUnitAt(0),
- r'"'.codeUnitAt(0),
- r'*'.codeUnitAt(0),
- r'+'.codeUnitAt(0),
- r'/'.codeUnitAt(0),
- r'\'.codeUnitAt(0),
- };
- BufferRangeLine? getWordBoundary(CellOffset position) {
- var separators = wordSeparators ?? defaultWordSeparators;
- if (position.y >= lines.length) {
- return null;
- }
- var line = lines[position.y];
- var start = position.x;
- var end = position.x;
- do {
- if (start == 0) {
- break;
- }
- final char = line.getCodePoint(start - 1);
- if (separators.contains(char)) {
- break;
- }
- start--;
- } while (true);
- do {
- if (end >= viewWidth) {
- break;
- }
- final char = line.getCodePoint(end);
- if (separators.contains(char)) {
- break;
- }
- end++;
- } while (true);
- if (start == end) {
- return null;
- }
- return BufferRangeLine(
- CellOffset(start, position.y),
- CellOffset(end, position.y),
- );
- }
- /// Get the plain text content of the buffer including the scrollback.
- /// Accepts an optional [range] to get a specific part of the buffer.
- String getText([BufferRange? range]) {
- range ??= BufferRangeLine(
- CellOffset(0, 0),
- CellOffset(viewWidth - 1, height - 1),
- );
- range = range.normalized;
- final builder = StringBuffer();
- for (var segment in range.toSegments()) {
- if (segment.line < 0 || segment.line >= height) {
- continue;
- }
- final line = lines[segment.line];
- if (!(segment.line == range.begin.y ||
- segment.line == 0 ||
- line.isWrapped)) {
- builder.write("\n");
- }
- builder.write(line.getText(segment.start, segment.end));
- }
- return builder.toString();
- }
- /// Returns a debug representation of the buffer.
- @override
- String toString() {
- final builder = StringBuffer();
- final lineNumberLength = lines.length.toString().length;
- for (var i = 0; i < lines.length; i++) {
- final line = lines[i];
- builder.write('${i.toString().padLeft(lineNumberLength)}: |${lines[i]}|');
- if (line.isWrapped) {
- builder.write(' (⏎)');
- }
- builder.write('\n');
- }
- return builder.toString();
- }
- }
|