terminal_isolate.dart 11 KB

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