buffer.dart 14 KB

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