buffer.dart 13 KB

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