import 'dart:math' show max, min; import 'package:xterm/core/buffer/position.dart'; import 'package:xterm/core/buffer/range.dart'; import 'package:xterm/core/cursor.dart'; import 'package:xterm/core/buffer/line.dart'; import 'package:xterm/core/reflow.dart'; import 'package:xterm/core/state.dart'; import 'package:xterm/core/charset.dart'; import 'package:xterm/utils/circular_list.dart'; import 'package:xterm/utils/unicode_v11.dart'; class Buffer { final TerminalState terminal; final int maxLines; final bool isAltBuffer; Buffer( this.terminal, { required this.maxLines, required this.isAltBuffer, }) { 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 = CircularList(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; 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); for (var i = 0; i < count; i++) { final shiftStart = absoluteCursorY; final shiftCount = absoluteMarginBottom - absoluteCursorY; lines.shiftElements(shiftStart, shiftCount, 1); lines[absoluteCursorY] = _newEmptyLine(); } } void deleteLines(int count) { if (!isInVerticalMargin) { return; } setCursorX(0); for (var i = 0; i < count; i++) { lines.insert(absoluteMarginBottom, _newEmptyLine()); lines.remove(absoluteCursorY); } } void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) { if (newWidth > oldWidth) { lines.forEach((item) => item.resize(newWidth)); } if (newHeight > oldHeight) { // Grow larger for (var i = 0; i < newHeight - oldHeight; i++) { if (newHeight > lines.length) { lines.push(_newEmptyLine()); } else { _cursorY++; } } } else { // Shrink smallerclear 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); if (!isAltBuffer && newWidth != oldWidth) { final reflowResult = reflow(lines, oldWidth, newWidth); while (reflowResult.length < newHeight) { reflowResult.add(_newEmptyLine()); } lines.replaceWith(reflowResult); } } BufferLine _newEmptyLine() { final line = BufferLine(viewWidth); return line; } static final _kWordSeparators = { 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), }; BufferRange? getWordBoundary(BufferPosition position) { 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 (_kWordSeparators.contains(char)) { break; } start--; } while (true); do { if (end >= viewWidth) { break; } final char = line.getCodePoint(end); if (_kWordSeparators.contains(char)) { break; } end++; } while (true); return BufferRange( BufferPosition(start, position.y), BufferPosition(end, position.y), ); } String getText(BufferRange range) { final builder = StringBuffer(); for (var i = range.begin.y; i <= range.end.y; i++) { if (i < 0 || i >= lines.length) { break; } final line = lines[i]; final start = i == range.begin.y ? range.begin.x : 0; final end = i == range.end.y ? range.end.x : line.length; if (i != range.begin.y && line.isWrapped) { builder.write('\n'); } builder.write(line.getText(start, end)); } return builder.toString(); } @override String toString() { final builder = StringBuffer(); final lineNumberLength = lines.length.toString().length; for (var i = 0; i < lines.length; i++) { builder.writeln('${i.toString().padLeft(lineNumberLength)}: ${lines[i]}'); } return builder.toString(); } }