terminal_isolate.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. import 'dart:async';
  2. import 'dart:isolate';
  3. import 'package:xterm/buffer/buffer_line.dart';
  4. import 'package:xterm/input/keys.dart';
  5. import 'package:xterm/mouse/position.dart';
  6. import 'package:xterm/mouse/selection.dart';
  7. import 'package:xterm/terminal/platform.dart';
  8. import 'package:xterm/terminal/terminal.dart';
  9. import 'package:xterm/terminal/terminal_backend.dart';
  10. import 'package:xterm/terminal/terminal_ui_interaction.dart';
  11. import 'package:xterm/theme/terminal_theme.dart';
  12. import 'package:xterm/theme/terminal_themes.dart';
  13. import 'package:xterm/util/observable.dart';
  14. void terminalMain(SendPort port) async {
  15. final rp = ReceivePort();
  16. port.send(rp.sendPort);
  17. Terminal? _terminal;
  18. await for (var msg in rp) {
  19. final String action = msg[0];
  20. switch (action) {
  21. case 'sendPort':
  22. port = msg[1];
  23. break;
  24. case 'init':
  25. final TerminalInitData initData = msg[1];
  26. _terminal = Terminal(
  27. backend: initData.backend,
  28. onTitleChange: (String title) {
  29. port.send(['onTitleChange', title]);
  30. },
  31. onIconChange: (String icon) {
  32. port.send(['onIconChange', icon]);
  33. },
  34. onBell: () {
  35. port.send(['onBell']);
  36. },
  37. platform: initData.platform,
  38. theme: initData.theme,
  39. maxLines: initData.maxLines);
  40. _terminal.addListener(() {
  41. port.send(['notify']);
  42. });
  43. initData.backend?.exitCode.then((value) => port.send(['exit', value]));
  44. port.send(['notify']);
  45. break;
  46. case 'write':
  47. _terminal?.write(msg[1]);
  48. break;
  49. case 'refresh':
  50. _terminal?.refresh();
  51. break;
  52. case 'selection.clear':
  53. _terminal?.selection!.clear();
  54. break;
  55. case 'mouseMode.onTap':
  56. _terminal?.mouseMode.onTap(_terminal, msg[1]);
  57. break;
  58. case 'mouseMode.onPanStart':
  59. _terminal?.mouseMode.onPanStart(_terminal, msg[1]);
  60. break;
  61. case 'mouseMode.onPanUpdate':
  62. _terminal?.mouseMode.onPanUpdate(_terminal, msg[1]);
  63. break;
  64. case 'setScrollOffsetFromBottom':
  65. _terminal?.setScrollOffsetFromBottom(msg[1]);
  66. break;
  67. case 'resize':
  68. _terminal?.resize(msg[1], msg[2]);
  69. break;
  70. case 'onInput':
  71. _terminal?.backend?.write(msg[1]);
  72. break;
  73. case 'keyInput':
  74. if (_terminal == null) {
  75. break;
  76. }
  77. _terminal.keyInput(msg[1],
  78. ctrl: msg[2], alt: msg[3], shift: msg[4], mac: msg[5]);
  79. break;
  80. case 'requestNewStateWhenDirty':
  81. if (_terminal == null) {
  82. break;
  83. }
  84. if (_terminal.dirty) {
  85. final newState = TerminalState(
  86. _terminal.buffer.scrollOffsetFromBottom,
  87. _terminal.buffer.scrollOffsetFromTop,
  88. _terminal.buffer.height,
  89. _terminal.invisibleHeight,
  90. _terminal.viewHeight,
  91. _terminal.viewWidth,
  92. _terminal.selection!,
  93. _terminal.getSelectedText(),
  94. _terminal.theme.background,
  95. _terminal.cursorX,
  96. _terminal.cursorY,
  97. _terminal.showCursor,
  98. _terminal.theme.cursor,
  99. _terminal.getVisibleLines(),
  100. _terminal.scrollOffset);
  101. port.send(['newState', newState]);
  102. }
  103. break;
  104. case 'paste':
  105. if (_terminal == null) {
  106. break;
  107. }
  108. _terminal.paste(msg[1]);
  109. break;
  110. }
  111. }
  112. }
  113. class TerminalInitData {
  114. PlatformBehavior platform;
  115. TerminalTheme theme;
  116. int maxLines;
  117. TerminalBackend? backend;
  118. TerminalInitData(this.backend, this.platform, this.theme, this.maxLines);
  119. }
  120. class TerminalState {
  121. int scrollOffsetFromTop;
  122. int scrollOffsetFromBottom;
  123. int bufferHeight;
  124. int invisibleHeight;
  125. int viewHeight;
  126. int viewWidth;
  127. Selection selection;
  128. String? selectedText;
  129. int backgroundColor;
  130. int cursorX;
  131. int cursorY;
  132. bool showCursor;
  133. int cursorColor;
  134. List<BufferLine> visibleLines;
  135. int scrollOffset;
  136. bool consumed = false;
  137. TerminalState(
  138. this.scrollOffsetFromBottom,
  139. this.scrollOffsetFromTop,
  140. this.bufferHeight,
  141. this.invisibleHeight,
  142. this.viewHeight,
  143. this.viewWidth,
  144. this.selection,
  145. this.selectedText,
  146. this.backgroundColor,
  147. this.cursorX,
  148. this.cursorY,
  149. this.showCursor,
  150. this.cursorColor,
  151. this.visibleLines,
  152. this.scrollOffset);
  153. }
  154. void _defaultBellHandler() {}
  155. void _defaultTitleHandler(String _) {}
  156. void _defaultIconHandler(String _) {}
  157. class TerminalIsolate with Observable implements TerminalUiInteraction {
  158. final _receivePort = ReceivePort();
  159. SendPort? _sendPort;
  160. late Isolate _isolate;
  161. final TerminalBackend? backend;
  162. final BellHandler onBell;
  163. final TitleChangeHandler onTitleChange;
  164. final IconChangeHandler onIconChange;
  165. final PlatformBehavior _platform;
  166. final TerminalTheme theme;
  167. final int maxLines;
  168. TerminalState? _lastState;
  169. final _backendExited = Completer<int>();
  170. Future<int> get backendExited => _backendExited.future;
  171. TerminalState? get lastState {
  172. return _lastState;
  173. }
  174. TerminalIsolate(
  175. {this.backend,
  176. this.onBell = _defaultBellHandler,
  177. this.onTitleChange = _defaultTitleHandler,
  178. this.onIconChange = _defaultIconHandler,
  179. PlatformBehavior platform = PlatformBehaviors.unix,
  180. this.theme = TerminalThemes.defaultTheme,
  181. required this.maxLines})
  182. : _platform = platform;
  183. @override
  184. int get scrollOffsetFromBottom => _lastState!.scrollOffsetFromBottom;
  185. @override
  186. int get scrollOffsetFromTop => _lastState!.scrollOffsetFromTop;
  187. @override
  188. int get scrollOffset => _lastState!.scrollOffset;
  189. @override
  190. int get bufferHeight => _lastState!.bufferHeight;
  191. @override
  192. int get terminalHeight => _lastState!.viewHeight;
  193. @override
  194. int get terminalWidth => _lastState!.viewWidth;
  195. @override
  196. int get invisibleHeight => _lastState!.invisibleHeight;
  197. @override
  198. Selection? get selection => _lastState?.selection;
  199. @override
  200. bool get showCursor => _lastState?.showCursor ?? true;
  201. @override
  202. List<BufferLine> getVisibleLines() {
  203. if (_lastState == null) {
  204. return List<BufferLine>.empty();
  205. }
  206. return _lastState!.visibleLines;
  207. }
  208. @override
  209. int get cursorY => _lastState?.cursorY ?? 0;
  210. @override
  211. int get cursorX => _lastState?.cursorX ?? 0;
  212. @override
  213. BufferLine? get currentLine {
  214. if (_lastState == null) {
  215. return null;
  216. }
  217. int visibleLineIndex =
  218. _lastState!.cursorY - _lastState!.scrollOffsetFromTop;
  219. if (visibleLineIndex < 0) {
  220. visibleLineIndex = _lastState!.cursorY;
  221. }
  222. return _lastState!.visibleLines[visibleLineIndex];
  223. }
  224. @override
  225. int get cursorColor => _lastState?.cursorColor ?? 0;
  226. @override
  227. int get backgroundColor => _lastState?.backgroundColor ?? 0;
  228. @override
  229. bool get dirty {
  230. if (_lastState == null) {
  231. return false;
  232. }
  233. if (_lastState!.consumed) {
  234. return false;
  235. }
  236. _lastState!.consumed = true;
  237. return true;
  238. }
  239. @override
  240. PlatformBehavior get platform => _platform;
  241. @override
  242. bool get isReady => _lastState != null;
  243. void start() async {
  244. final initialRefreshCompleted = Completer<bool>();
  245. var firstReceivePort = ReceivePort();
  246. _isolate = await Isolate.spawn(terminalMain, firstReceivePort.sendPort);
  247. _sendPort = await firstReceivePort.first;
  248. _sendPort!.send(['sendPort', _receivePort.sendPort]);
  249. _receivePort.listen((message) {
  250. String action = message[0];
  251. switch (action) {
  252. case 'onBell':
  253. this.onBell();
  254. break;
  255. case 'onTitleChange':
  256. this.onTitleChange(message[1]);
  257. break;
  258. case 'onIconChange':
  259. this.onIconChange(message[1]);
  260. break;
  261. case 'notify':
  262. poll();
  263. break;
  264. case 'newState':
  265. _lastState = message[1];
  266. if (!initialRefreshCompleted.isCompleted) {
  267. initialRefreshCompleted.complete(true);
  268. }
  269. this.notifyListeners();
  270. break;
  271. case 'exit':
  272. _backendExited.complete(message[1]);
  273. break;
  274. }
  275. });
  276. _sendPort!.send([
  277. 'init',
  278. TerminalInitData(this.backend, this.platform, this.theme, this.maxLines)
  279. ]);
  280. await initialRefreshCompleted.future;
  281. }
  282. void stop() {
  283. _isolate.kill();
  284. }
  285. void poll() {
  286. if (_sendPort == null) {
  287. return;
  288. }
  289. _sendPort!.send(['requestNewStateWhenDirty']);
  290. }
  291. void refresh() {
  292. if (_sendPort == null) {
  293. return;
  294. }
  295. _sendPort!.send(['refresh']);
  296. }
  297. void clearSelection() {
  298. if (_sendPort == null) {
  299. return;
  300. }
  301. _sendPort!.send(['clearSelection']);
  302. }
  303. void onMouseTap(Position position) {
  304. if (_sendPort == null) {
  305. return;
  306. }
  307. _sendPort!.send(['mouseMode.onTap', position]);
  308. }
  309. void onPanStart(Position position) {
  310. if (_sendPort == null) {
  311. return;
  312. }
  313. _sendPort!.send(['mouseMode.onPanStart', position]);
  314. }
  315. void onPanUpdate(Position position) {
  316. if (_sendPort == null) {
  317. return;
  318. }
  319. _sendPort!.send(['mouseMode.onPanUpdate', position]);
  320. }
  321. void setScrollOffsetFromBottom(int offset) {
  322. if (_sendPort == null) {
  323. return;
  324. }
  325. _sendPort!.send(['setScrollOffsetFromBottom', offset]);
  326. }
  327. int convertViewLineToRawLine(int viewLine) {
  328. if (_lastState == null) {
  329. return 0;
  330. }
  331. if (_lastState!.viewHeight > _lastState!.bufferHeight) {
  332. return viewLine;
  333. }
  334. return viewLine + (_lastState!.bufferHeight - _lastState!.viewHeight);
  335. }
  336. void write(String text) {
  337. if (_sendPort == null) {
  338. return;
  339. }
  340. _sendPort!.send(['write', text]);
  341. }
  342. void paste(String data) {
  343. if (_sendPort == null) {
  344. return;
  345. }
  346. _sendPort!.send(['paste', data]);
  347. }
  348. void resize(int newWidth, int newHeight) {
  349. if (_sendPort == null) {
  350. return;
  351. }
  352. _sendPort!.send(['resize', newWidth, newHeight]);
  353. }
  354. void raiseOnInput(String text) {
  355. _sendPort!.send(['onInput', text]);
  356. }
  357. void keyInput(
  358. TerminalKey key, {
  359. bool ctrl = false,
  360. bool alt = false,
  361. bool shift = false,
  362. bool mac = false,
  363. // bool meta,
  364. }) {
  365. if (_sendPort == null) {
  366. return;
  367. }
  368. _sendPort!.send(['keyInput', key, ctrl, alt, shift, mac]);
  369. }
  370. }