zmodem.dart 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. import 'dart:async';
  2. import 'dart:io';
  3. import 'dart:typed_data';
  4. import 'package:dartssh2/dartssh2.dart';
  5. import 'package:example/src/virtual_keyboard.dart';
  6. import 'package:file_picker/file_picker.dart';
  7. import 'package:flutter/cupertino.dart';
  8. import 'package:path/path.dart' as path;
  9. import 'package:xterm/xterm.dart';
  10. const host = 'localhost';
  11. const port = 22;
  12. const username = '<your username>';
  13. const password = '<your password>';
  14. void main() {
  15. runApp(MyApp());
  16. }
  17. class MyApp extends StatelessWidget {
  18. @override
  19. Widget build(BuildContext context) {
  20. return CupertinoApp(
  21. title: 'xterm.dart demo',
  22. home: MyHomePage(),
  23. );
  24. }
  25. }
  26. class MyHomePage extends StatefulWidget {
  27. MyHomePage({Key? key}) : super(key: key);
  28. @override
  29. // ignore: library_private_types_in_public_api
  30. _MyHomePageState createState() => _MyHomePageState();
  31. }
  32. class _MyHomePageState extends State<MyHomePage> {
  33. late final terminal = Terminal(inputHandler: keyboard);
  34. final keyboard = VirtualKeyboard(defaultInputHandler);
  35. var title = host;
  36. @override
  37. void initState() {
  38. super.initState();
  39. initTerminal();
  40. }
  41. Future<void> initTerminal() async {
  42. terminal.write('Connecting...\r\n');
  43. final client = SSHClient(
  44. await SSHSocket.connect(host, port),
  45. username: username,
  46. onPasswordRequest: () => password,
  47. );
  48. terminal.write('Connected\r\n');
  49. final session = await client.shell(
  50. pty: SSHPtyConfig(
  51. width: terminal.viewWidth,
  52. height: terminal.viewHeight,
  53. ),
  54. );
  55. terminal.buffer.clear();
  56. terminal.buffer.setCursor(0, 0);
  57. terminal.onTitleChange = (title) {
  58. setState(() => this.title = title);
  59. };
  60. terminal.onResize = (width, height, pixelWidth, pixelHeight) {
  61. session.resizeTerminal(width, height, pixelWidth, pixelHeight);
  62. };
  63. final mux = ZModemMux(
  64. stdin: session.stdin,
  65. stdout: session.stdout,
  66. );
  67. mux.onTerminalInput = terminal.write;
  68. mux.onFileOffer = _handleFileOffer;
  69. mux.onFileRequest = _handleFileRequest;
  70. terminal.onOutput = mux.terminalWrite;
  71. }
  72. void _handleFileOffer(ZModemOffer offer) async {
  73. print(offer.info);
  74. final outputDir = await FilePicker.platform.getDirectoryPath();
  75. if (outputDir == null) {
  76. offer.skip();
  77. return;
  78. }
  79. final file = File(path.join(outputDir, offer.info.pathname));
  80. void updateProgress(int received) {
  81. final length = offer.info.length;
  82. if (length != null) {
  83. terminal.write('\r');
  84. terminal.write('\x1b[K');
  85. terminal.write('${offer.info.pathname}: ');
  86. terminal.write((received / length * 100).toStringAsFixed(1));
  87. terminal.write('%');
  88. }
  89. }
  90. await offer
  91. .accept(0)
  92. .cast<List<int>>()
  93. .transform(WithProgress(onProgress: updateProgress))
  94. .pipe(file.openWrite());
  95. terminal.write('\r\n');
  96. terminal.write('Received ${offer.info.pathname}');
  97. }
  98. Future<Iterable<ZModemOffer>> _handleFileRequest() async {
  99. final result = await FilePicker.platform.pickFiles(withReadStream: true);
  100. if (result == null) {
  101. return [];
  102. }
  103. void updateProgress(PlatformFile file, int received) {
  104. terminal.write('\r');
  105. terminal.write('\x1b[K');
  106. terminal.write('${file.name}: ');
  107. terminal.write((received / file.size * 100).toStringAsFixed(1));
  108. terminal.write('%');
  109. }
  110. return result.files.map(
  111. (file) => ZModemCallbackOffer(
  112. ZModemFileInfo(
  113. pathname: path.basename(file.path!),
  114. length: file.size,
  115. mode: '100644',
  116. filesRemaining: 1,
  117. bytesRemaining: file.size,
  118. ),
  119. onAccept: (offset) => file.readStream!
  120. .skip(offset)
  121. .transform(
  122. WithProgress(onProgress: (bytes) => updateProgress(file, bytes)),
  123. )
  124. .cast<Uint8List>(),
  125. ),
  126. );
  127. }
  128. @override
  129. Widget build(BuildContext context) {
  130. return CupertinoPageScaffold(
  131. navigationBar: CupertinoNavigationBar(
  132. middle: Text(title),
  133. backgroundColor:
  134. CupertinoTheme.of(context).barBackgroundColor.withOpacity(0.5),
  135. ),
  136. child: Column(
  137. children: [
  138. Expanded(
  139. child: TerminalView(terminal),
  140. ),
  141. VirtualKeyboardView(keyboard),
  142. ],
  143. ),
  144. );
  145. }
  146. }
  147. class WithProgress<T> extends StreamTransformerBase<List<T>, List<T>> {
  148. WithProgress({this.onProgress});
  149. void Function(int progress)? onProgress;
  150. var _progress = 0;
  151. int get progress => _progress;
  152. @override
  153. Stream<List<T>> bind(Stream<List<T>> stream) {
  154. return stream.transform(StreamTransformer<List<T>, List<T>>.fromHandlers(
  155. handleData: (List<T> data, EventSink<List<T>> sink) {
  156. _progress += data.length;
  157. onProgress?.call(_progress);
  158. sink.add(data);
  159. },
  160. ));
  161. }
  162. }