terminal_isolate.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  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. TerminalState? get lastState {
  197. return _lastState;
  198. }
  199. TerminalIsolate(
  200. {this.backend,
  201. this.onBell = _defaultBellHandler,
  202. this.onTitleChange = _defaultTitleHandler,
  203. this.onIconChange = _defaultIconHandler,
  204. PlatformBehavior platform = PlatformBehaviors.unix,
  205. this.theme = TerminalThemes.defaultTheme,
  206. required this.maxLines})
  207. : _platform = platform;
  208. @override
  209. int get scrollOffsetFromBottom => _lastState!.scrollOffsetFromBottom;
  210. @override
  211. int get scrollOffsetFromTop => _lastState!.scrollOffsetFromTop;
  212. @override
  213. int get scrollOffset => _lastState!.scrollOffset;
  214. @override
  215. int get bufferHeight => _lastState!.bufferHeight;
  216. @override
  217. int get terminalHeight => _lastState!.viewHeight;
  218. @override
  219. int get terminalWidth => _lastState!.viewWidth;
  220. @override
  221. int get invisibleHeight => _lastState!.invisibleHeight;
  222. @override
  223. Selection? get selection => _lastState?.selection;
  224. @override
  225. bool get showCursor => _lastState?.showCursor ?? true;
  226. @override
  227. List<BufferLine> getVisibleLines() {
  228. if (_lastState == null) {
  229. return List<BufferLine>.empty();
  230. }
  231. return _lastState!.visibleLines;
  232. }
  233. @override
  234. int get cursorY => _lastState?.cursorY ?? 0;
  235. @override
  236. int get cursorX => _lastState?.cursorX ?? 0;
  237. @override
  238. BufferLine? get currentLine {
  239. if (_lastState == null) {
  240. return null;
  241. }
  242. int visibleLineIndex =
  243. _lastState!.cursorY - _lastState!.scrollOffsetFromTop;
  244. if (visibleLineIndex < 0) {
  245. visibleLineIndex = _lastState!.cursorY;
  246. }
  247. return _lastState!.visibleLines[visibleLineIndex];
  248. }
  249. @override
  250. int get cursorColor => _lastState?.cursorColor ?? 0;
  251. @override
  252. int get backgroundColor => _lastState?.backgroundColor ?? 0;
  253. @override
  254. bool get dirty {
  255. if (_lastState == null) {
  256. return false;
  257. }
  258. if (_lastState!.consumed) {
  259. return false;
  260. }
  261. _lastState!.consumed = true;
  262. return true;
  263. }
  264. @override
  265. PlatformBehavior get platform => _platform;
  266. @override
  267. bool get isReady => _lastState != null;
  268. void start() async {
  269. final initialRefreshCompleted = Completer<bool>();
  270. var firstReceivePort = ReceivePort();
  271. _isolate = await Isolate.spawn(terminalMain, firstReceivePort.sendPort);
  272. _sendPort = await firstReceivePort.first;
  273. _sendPort!.send([IsolateCommand.SendPort, _receivePort.sendPort]);
  274. _receivePort.listen((message) {
  275. IsolateEvent action = message[0];
  276. switch (action) {
  277. case IsolateEvent.Bell:
  278. this.onBell();
  279. break;
  280. case IsolateEvent.TitleChanged:
  281. this.onTitleChange(message[1]);
  282. break;
  283. case IsolateEvent.IconChanged:
  284. this.onIconChange(message[1]);
  285. break;
  286. case IsolateEvent.NotifyChange:
  287. poll();
  288. break;
  289. case IsolateEvent.NewState:
  290. _lastState = message[1];
  291. if (!initialRefreshCompleted.isCompleted) {
  292. initialRefreshCompleted.complete(true);
  293. }
  294. this.notifyListeners();
  295. break;
  296. case IsolateEvent.Exit:
  297. _backendExited.complete(message[1]);
  298. break;
  299. }
  300. });
  301. _sendPort!.send([
  302. IsolateCommand.Init,
  303. TerminalInitData(this.backend, this.platform, this.theme, this.maxLines)
  304. ]);
  305. await initialRefreshCompleted.future;
  306. }
  307. void stop() {
  308. _isolate.kill();
  309. }
  310. void poll() {
  311. if (_sendPort == null) {
  312. return;
  313. }
  314. _sendPort!.send([IsolateCommand.RequestNewStateWhenDirty]);
  315. }
  316. void refresh() {
  317. if (_sendPort == null) {
  318. return;
  319. }
  320. _sendPort!.send([IsolateCommand.Refresh]);
  321. }
  322. void clearSelection() {
  323. if (_sendPort == null) {
  324. return;
  325. }
  326. _sendPort!.send([IsolateCommand.ClearSelection]);
  327. }
  328. void onMouseTap(Position position) {
  329. if (_sendPort == null) {
  330. return;
  331. }
  332. _sendPort!.send([IsolateCommand.MouseTap, position]);
  333. }
  334. void onPanStart(Position position) {
  335. if (_sendPort == null) {
  336. return;
  337. }
  338. _sendPort!.send([IsolateCommand.MousePanStart, position]);
  339. }
  340. void onPanUpdate(Position position) {
  341. if (_sendPort == null) {
  342. return;
  343. }
  344. _sendPort!.send([IsolateCommand.MousePanUpdate, position]);
  345. }
  346. void setScrollOffsetFromBottom(int offset) {
  347. if (_sendPort == null) {
  348. return;
  349. }
  350. _sendPort!.send([IsolateCommand.SetScrollOffsetFromTop, offset]);
  351. }
  352. int convertViewLineToRawLine(int viewLine) {
  353. if (_lastState == null) {
  354. return 0;
  355. }
  356. if (_lastState!.viewHeight > _lastState!.bufferHeight) {
  357. return viewLine;
  358. }
  359. return viewLine + (_lastState!.bufferHeight - _lastState!.viewHeight);
  360. }
  361. void write(String text) {
  362. if (_sendPort == null) {
  363. return;
  364. }
  365. _sendPort!.send([IsolateCommand.Write, text]);
  366. }
  367. void paste(String data) {
  368. if (_sendPort == null) {
  369. return;
  370. }
  371. _sendPort!.send([IsolateCommand.Paste, data]);
  372. }
  373. void resize(int newWidth, int newHeight) {
  374. if (_sendPort == null) {
  375. return;
  376. }
  377. _sendPort!.send([IsolateCommand.Resize, newWidth, newHeight]);
  378. }
  379. void raiseOnInput(String text) {
  380. _sendPort!.send([IsolateCommand.OnInput, text]);
  381. }
  382. void keyInput(
  383. TerminalKey key, {
  384. bool ctrl = false,
  385. bool alt = false,
  386. bool shift = false,
  387. bool mac = false,
  388. // bool meta,
  389. }) {
  390. if (_sendPort == null) {
  391. return;
  392. }
  393. _sendPort!.send([IsolateCommand.KeyInput, key, ctrl, alt, shift, mac]);
  394. }
  395. }