terminal_isolate.dart 12 KB

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