buffer.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. import 'dart:math' show max, min;
  2. import 'package:flutter/material.dart';
  3. import 'package:xterm/src/core/buffer/cell_offset.dart';
  4. import 'package:xterm/src/core/buffer/line.dart';
  5. import 'package:xterm/src/core/buffer/range_line.dart';
  6. import 'package:xterm/src/core/buffer/range.dart';
  7. import 'package:xterm/src/core/charset.dart';
  8. import 'package:xterm/src/core/cursor.dart';
  9. import 'package:xterm/src/core/reflow.dart';
  10. import 'package:xterm/src/core/state.dart';
  11. import 'package:xterm/src/utils/circular_list.dart';
  12. import 'package:xterm/src/utils/unicode_v11.dart';
  13. class Buffer {
  14. final TerminalState terminal;
  15. final int maxLines;
  16. final bool isAltBuffer;
  17. Buffer(
  18. this.terminal, {
  19. required this.maxLines,
  20. required this.isAltBuffer,
  21. }) {
  22. for (int i = 0; i < terminal.viewHeight; i++) {
  23. lines.push(_newEmptyLine());
  24. }
  25. resetVerticalMargins();
  26. }
  27. int _cursorX = 0;
  28. int _cursorY = 0;
  29. late int _marginTop;
  30. late int _marginBottom;
  31. var _savedCursorX = 0;
  32. var _savedCursorY = 0;
  33. final _savedCursorStyle = CursorStyle();
  34. final charset = Charset();
  35. /// Width of the viewport in columns. Also the index of the last column.
  36. int get viewWidth => terminal.viewWidth;
  37. /// Height of the viewport in rows. Also the index of the last line.
  38. int get viewHeight => terminal.viewHeight;
  39. /// lines of the buffer. the length of [lines] should always be equal or
  40. /// greater than [viewHeight].
  41. late final lines = CircularList<BufferLine>(maxLines);
  42. /// Total number of lines in the buffer. Always equal or greater than
  43. /// [viewHeight].
  44. int get height => lines.length;
  45. /// Horizontal position of the cursor relative to the top-left cornor of the
  46. /// screen, starting from 0.
  47. int get cursorX => _cursorX.clamp(0, terminal.viewWidth - 1);
  48. /// Vertical position of the cursor relative to the top-left cornor of the
  49. /// screen, starting from 0.
  50. int get cursorY => _cursorY;
  51. /// Index of the first line in the scroll region.
  52. int get marginTop => _marginTop;
  53. /// Index of the last line in the scroll region.
  54. int get marginBottom => _marginBottom;
  55. /// The number of lines above the viewport.
  56. int get scrollBack => height - viewHeight;
  57. /// Vertical position of the cursor relative to the top of the buffer,
  58. /// starting from 0.
  59. int get absoluteCursorY => _cursorY + scrollBack;
  60. /// Absolute index of the first line in the scroll region.
  61. int get absoluteMarginTop => _marginTop + scrollBack;
  62. /// Absolute index of the last line in the scroll region.
  63. int get absoluteMarginBottom => _marginBottom + scrollBack;
  64. /// Writes data to the _terminal. Terminal sequences or special characters are
  65. /// not interpreted and directly added to the buffer.
  66. ///
  67. /// See also: [Terminal.write]
  68. void write(String text) {
  69. for (var char in text.runes) {
  70. writeChar(char);
  71. }
  72. }
  73. /// Writes a single character to the _terminal. Escape sequences or special
  74. /// characters are not interpreted and directly added to the buffer.
  75. ///
  76. /// See also: [Terminal.writeChar]
  77. void writeChar(int codePoint) {
  78. codePoint = charset.translate(codePoint);
  79. final cellWidth = unicodeV11.wcwidth(codePoint);
  80. if (_cursorX >= terminal.viewWidth) {
  81. index();
  82. setCursorX(0);
  83. if (terminal.autoWrapMode) {
  84. currentLine.isWrapped = true;
  85. }
  86. }
  87. final line = currentLine;
  88. line.setCell(_cursorX, codePoint, cellWidth, terminal.cursor);
  89. if (_cursorX < viewWidth) {
  90. _cursorX++;
  91. }
  92. if (cellWidth == 2) {
  93. writeChar(0);
  94. }
  95. }
  96. /// The line at the current cursor position.
  97. BufferLine get currentLine {
  98. return lines[absoluteCursorY];
  99. }
  100. void backspace() {
  101. if (_cursorX == 0 && currentLine.isWrapped) {
  102. currentLine.isWrapped = false;
  103. moveCursor(viewWidth - 1, -1);
  104. } else if (_cursorX == viewWidth) {
  105. moveCursor(-2, 0);
  106. } else {
  107. moveCursor(-1, 0);
  108. }
  109. }
  110. /// Erases the viewport from the cursor position to the end of the buffer,
  111. /// including the cursor position.
  112. void eraseDisplayFromCursor() {
  113. eraseLineFromCursor();
  114. for (var i = absoluteCursorY; i < height; i++) {
  115. final line = lines[i];
  116. line.isWrapped = false;
  117. line.eraseRange(0, viewWidth, terminal.cursor);
  118. }
  119. }
  120. /// Erases the viewport from the top-left corner to the cursor, including the
  121. /// cursor.
  122. void eraseDisplayToCursor() {
  123. eraseLineToCursor();
  124. for (var i = 0; i < _cursorY; i++) {
  125. final line = lines[i + scrollBack];
  126. line.isWrapped = false;
  127. line.eraseRange(0, viewWidth, terminal.cursor);
  128. }
  129. }
  130. /// Erases the whole viewport.
  131. void eraseDisplay() {
  132. for (var i = 0; i < viewHeight; i++) {
  133. final line = lines[i + scrollBack];
  134. line.isWrapped = false;
  135. line.eraseRange(0, viewWidth, terminal.cursor);
  136. }
  137. }
  138. /// Erases the line from the cursor to the end of the line, including the
  139. /// cursor position.
  140. void eraseLineFromCursor() {
  141. currentLine.isWrapped = false;
  142. currentLine.eraseRange(_cursorX, viewWidth, terminal.cursor);
  143. }
  144. /// Erases the line from the start of the line to the cursor, including the
  145. /// cursor.
  146. void eraseLineToCursor() {
  147. currentLine.isWrapped = false;
  148. currentLine.eraseRange(0, _cursorX, terminal.cursor);
  149. }
  150. /// Erases the line at the current cursor position.
  151. void eraseLine() {
  152. currentLine.isWrapped = false;
  153. currentLine.eraseRange(0, viewWidth, terminal.cursor);
  154. }
  155. /// Erases [count] cells starting at the cursor position.
  156. void eraseChars(int count) {
  157. final start = _cursorX;
  158. currentLine.eraseRange(start, start + count, terminal.cursor);
  159. }
  160. void scrollDown(int lines) {
  161. for (var i = absoluteMarginBottom; i >= absoluteMarginTop; i--) {
  162. if (i >= absoluteMarginTop + lines) {
  163. this.lines[i] = this.lines[i - lines];
  164. } else {
  165. this.lines[i] = _newEmptyLine();
  166. }
  167. }
  168. }
  169. void scrollUp(int lines) {
  170. for (var i = absoluteMarginTop; i <= absoluteMarginBottom; i++) {
  171. if (i <= absoluteMarginBottom - lines) {
  172. this.lines[i] = this.lines[i + lines];
  173. } else {
  174. this.lines[i] = _newEmptyLine();
  175. }
  176. }
  177. }
  178. /// https://vt100.net/docs/vt100-ug/chapter3.html#IND IND – Index
  179. ///
  180. /// ESC D
  181. ///
  182. /// [index] causes the active position to move downward one line without
  183. /// changing the column position. If the active position is at the bottom
  184. /// margin, a scroll up is performed.
  185. void index() {
  186. if (isInVerticalMargin) {
  187. if (_cursorY == _marginBottom) {
  188. if (marginTop == 0 && !isAltBuffer) {
  189. lines.insert(absoluteMarginBottom + 1, _newEmptyLine());
  190. } else {
  191. scrollUp(1);
  192. }
  193. } else {
  194. moveCursorY(1);
  195. }
  196. return;
  197. }
  198. // the cursor is not in the scrollable region
  199. if (_cursorY >= viewHeight - 1) {
  200. // we are at the bottom
  201. if (isAltBuffer) {
  202. scrollUp(1);
  203. } else {
  204. lines.push(_newEmptyLine());
  205. }
  206. } else {
  207. // there're still lines so we simply move cursor down.
  208. moveCursorY(1);
  209. }
  210. }
  211. void lineFeed() {
  212. index();
  213. if (terminal.lineFeedMode) {
  214. setCursorX(0);
  215. }
  216. }
  217. /// https://terminalguide.namepad.de/seq/a_esc_cm/
  218. void reverseIndex() {
  219. if (isInVerticalMargin) {
  220. if (_cursorY == _marginTop) {
  221. scrollDown(1);
  222. } else {
  223. moveCursorY(-1);
  224. }
  225. } else {
  226. moveCursorY(-1);
  227. }
  228. }
  229. void cursorGoForward() {
  230. _cursorX = min(_cursorX + 1, viewWidth);
  231. }
  232. void setCursorX(int cursorX) {
  233. _cursorX = cursorX.clamp(0, viewWidth - 1);
  234. }
  235. void setCursorY(int cursorY) {
  236. _cursorY = cursorY.clamp(0, viewHeight - 1);
  237. }
  238. void moveCursorX(int offset) {
  239. setCursorX(_cursorX + offset);
  240. }
  241. void moveCursorY(int offset) {
  242. setCursorY(_cursorY + offset);
  243. }
  244. void setCursor(int cursorX, int cursorY) {
  245. var maxCursorY = viewHeight - 1;
  246. if (terminal.originMode) {
  247. cursorY += _marginTop;
  248. maxCursorY = _marginBottom;
  249. }
  250. _cursorX = cursorX.clamp(0, viewWidth - 1);
  251. _cursorY = cursorY.clamp(0, maxCursorY);
  252. }
  253. void moveCursor(int offsetX, int offsetY) {
  254. final cursorX = _cursorX + offsetX;
  255. final cursorY = _cursorY + offsetY;
  256. setCursor(cursorX, cursorY);
  257. }
  258. /// Save cursor position, charmap and text attributes.
  259. void saveCursor() {
  260. _savedCursorX = _cursorX;
  261. _savedCursorY = _cursorY;
  262. _savedCursorStyle.foreground = terminal.cursor.foreground;
  263. _savedCursorStyle.background = terminal.cursor.background;
  264. _savedCursorStyle.attrs = terminal.cursor.attrs;
  265. charset.save();
  266. }
  267. /// Restore cursor position, charmap and text attributes.
  268. void restoreCursor() {
  269. _cursorX = _savedCursorX;
  270. _cursorY = _savedCursorY;
  271. terminal.cursor.foreground = _savedCursorStyle.foreground;
  272. terminal.cursor.background = _savedCursorStyle.background;
  273. terminal.cursor.attrs = _savedCursorStyle.attrs;
  274. charset.restore();
  275. }
  276. /// Sets the vertical scrolling margin to [top] and [bottom].
  277. /// Both values must be between 0 and [viewHeight] - 1.
  278. void setVerticalMargins(int top, int bottom) {
  279. _marginTop = top.clamp(0, viewHeight - 1);
  280. _marginBottom = bottom.clamp(0, viewHeight - 1);
  281. _marginTop = min(_marginTop, _marginBottom);
  282. _marginBottom = max(_marginTop, _marginBottom);
  283. }
  284. bool get isInVerticalMargin {
  285. return _cursorY >= _marginTop && _cursorY <= _marginBottom;
  286. }
  287. void resetVerticalMargins() {
  288. setVerticalMargins(0, viewHeight - 1);
  289. }
  290. void deleteChars(int count) {
  291. final start = _cursorX.clamp(0, viewWidth);
  292. count = min(count, viewWidth - start);
  293. currentLine.removeCells(start, count, terminal.cursor);
  294. }
  295. /// Remove all lines above the top of the viewport.
  296. void clearScrollback() {
  297. if (height <= viewHeight) {
  298. return;
  299. }
  300. lines.trimStart(scrollBack);
  301. }
  302. /// Clears the viewport and scrollback buffer. Then fill with empty lines.
  303. void clear() {
  304. lines.clear();
  305. for (int i = 0; i < viewHeight; i++) {
  306. lines.push(_newEmptyLine());
  307. }
  308. }
  309. void insertBlankChars(int count) {
  310. currentLine.insertCells(_cursorX, count, terminal.cursor);
  311. }
  312. void insertLines(int count) {
  313. if (!isInVerticalMargin) {
  314. return;
  315. }
  316. setCursorX(0);
  317. for (var i = 0; i < count; i++) {
  318. final shiftStart = absoluteCursorY;
  319. final shiftCount = absoluteMarginBottom - absoluteCursorY;
  320. lines.shiftElements(shiftStart, shiftCount, 1);
  321. lines[absoluteCursorY] = _newEmptyLine();
  322. }
  323. }
  324. void deleteLines(int count) {
  325. if (!isInVerticalMargin) {
  326. return;
  327. }
  328. setCursorX(0);
  329. for (var i = 0; i < count; i++) {
  330. lines.insert(absoluteMarginBottom, _newEmptyLine());
  331. lines.remove(absoluteCursorY);
  332. }
  333. }
  334. void resize(int oldWidth, int oldHeight, int newWidth, int newHeight) {
  335. if (newWidth > oldWidth) {
  336. lines.forEach((item) => item.resize(newWidth));
  337. }
  338. if (newHeight > oldHeight) {
  339. // Grow larger
  340. for (var i = 0; i < newHeight - oldHeight; i++) {
  341. if (newHeight > lines.length) {
  342. lines.push(_newEmptyLine());
  343. } else {
  344. _cursorY++;
  345. }
  346. }
  347. } else {
  348. // Shrink smallerclear
  349. for (var i = 0; i < oldHeight - newHeight; i++) {
  350. if (_cursorY > newHeight - 1) {
  351. _cursorY--;
  352. } else {
  353. lines.pop();
  354. }
  355. }
  356. }
  357. // Ensure cursor is within the screen.
  358. _cursorX = _cursorX.clamp(0, newWidth - 1);
  359. _cursorY = _cursorY.clamp(0, newHeight - 1);
  360. if (!isAltBuffer && newWidth != oldWidth) {
  361. final reflowResult = reflow(lines, oldWidth, newWidth);
  362. while (reflowResult.length < newHeight) {
  363. reflowResult.add(_newEmptyLine());
  364. }
  365. lines.replaceWith(reflowResult);
  366. }
  367. }
  368. BufferLine _newEmptyLine() {
  369. final line = BufferLine(viewWidth);
  370. return line;
  371. }
  372. static final _kWordSeparators = <int>{
  373. 0,
  374. r' '.codeUnitAt(0),
  375. r'.'.codeUnitAt(0),
  376. r':'.codeUnitAt(0),
  377. r'-'.codeUnitAt(0),
  378. r'\'.codeUnitAt(0),
  379. r'"'.codeUnitAt(0),
  380. r'*'.codeUnitAt(0),
  381. r'+'.codeUnitAt(0),
  382. r'/'.codeUnitAt(0),
  383. r'\'.codeUnitAt(0),
  384. };
  385. BufferRangeLine? getWordBoundary(CellOffset position) {
  386. if (position.y >= lines.length) {
  387. return null;
  388. }
  389. var line = lines[position.y];
  390. var start = position.x;
  391. var end = position.x;
  392. do {
  393. if (start == 0) {
  394. break;
  395. }
  396. final char = line.getCodePoint(start - 1);
  397. if (_kWordSeparators.contains(char)) {
  398. break;
  399. }
  400. start--;
  401. } while (true);
  402. do {
  403. if (end >= viewWidth) {
  404. break;
  405. }
  406. final char = line.getCodePoint(end);
  407. if (_kWordSeparators.contains(char)) {
  408. break;
  409. }
  410. end++;
  411. } while (true);
  412. if (start == end) {
  413. return null;
  414. }
  415. return BufferRangeLine(
  416. CellOffset(start, position.y),
  417. CellOffset(end, position.y),
  418. );
  419. }
  420. /// Get the plain text content of the buffer including the scrollback.
  421. /// Accepts an optional [range] to get a specific part of the buffer.
  422. String getText([BufferRange? range]) {
  423. range ??= BufferRangeLine(
  424. CellOffset(0, 0),
  425. CellOffset(viewWidth - 1, height - 1),
  426. );
  427. range = range.normalized;
  428. final builder = StringBuffer();
  429. for (var segment in range.toSegments()) {
  430. if (segment.line < 0 || segment.line >= height) {
  431. continue;
  432. }
  433. final line = lines[segment.line];
  434. if (!(segment.line == range.begin.y ||
  435. segment.line == 0 ||
  436. line.isWrapped)) {
  437. builder.write("\n");
  438. }
  439. builder.write(line.getText(segment.start, segment.end));
  440. }
  441. return builder.toString();
  442. }
  443. /// Returns a debug representation of the buffer.
  444. @override
  445. String toString() {
  446. final builder = StringBuffer();
  447. final lineNumberLength = lines.length.toString().length;
  448. for (var i = 0; i < lines.length; i++) {
  449. final line = lines[i];
  450. builder.write('${i.toString().padLeft(lineNumberLength)}: |${lines[i]}|');
  451. TextEditingValue;
  452. if (line.isWrapped) {
  453. builder.write(' (⏎)');
  454. }
  455. builder.write('\n');
  456. }
  457. return builder.toString();
  458. }
  459. }