Skip to content

Commit 435a25d

Browse files
committed
ZModem
1 parent 7a32ef0 commit 435a25d

File tree

10 files changed

+830
-409
lines changed

10 files changed

+830
-409
lines changed

example/lib/main.dart

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,10 @@ import 'package:example/src/platform_menu.dart';
55
import 'package:flutter/foundation.dart';
66
import 'package:flutter/material.dart';
77
import 'package:flutter/services.dart';
8-
import 'package:flutter_acrylic/flutter_acrylic.dart';
98
import 'package:flutter_pty/flutter_pty.dart';
109
import 'package:xterm/xterm.dart';
1110

1211
void main() {
13-
WidgetsFlutterBinding.ensureInitialized();
14-
15-
if (isDesktop) {
16-
setupAcrylic();
17-
}
18-
1912
runApp(MyApp());
2013
}
2114

@@ -28,13 +21,6 @@ bool get isDesktop {
2821
].contains(defaultTargetPlatform);
2922
}
3023

31-
Future<void> setupAcrylic() async {
32-
await Window.initialize();
33-
await Window.makeTitlebarTransparent();
34-
await Window.setEffect(effect: WindowEffect.aero, color: Color(0xFFFFFFFF));
35-
await Window.setBlurViewState(MacOSBlurViewState.active);
36-
}
37-
3824
class MyApp extends StatelessWidget {
3925
@override
4026
Widget build(BuildContext context) {

example/lib/mock.dart

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,8 @@
1-
import 'dart:async';
2-
31
import 'package:flutter/foundation.dart';
42
import 'package:flutter/material.dart';
5-
import 'package:flutter_acrylic/flutter_acrylic.dart';
63
import 'package:xterm/xterm.dart';
74

85
void main() {
9-
WidgetsFlutterBinding.ensureInitialized();
10-
11-
if (isDesktop) {
12-
setupAcrylic();
13-
}
14-
156
runApp(MyApp());
167
}
178

@@ -24,13 +15,6 @@ bool get isDesktop {
2415
].contains(defaultTargetPlatform);
2516
}
2617

27-
Future<void> setupAcrylic() async {
28-
await Window.initialize();
29-
await Window.makeTitlebarTransparent();
30-
await Window.setEffect(effect: WindowEffect.aero);
31-
await Window.setBlurViewState(MacOSBlurViewState.active);
32-
}
33-
3418
class MyApp extends StatelessWidget {
3519
@override
3620
Widget build(BuildContext context) {

example/lib/zmodem.dart

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

0 commit comments

Comments
 (0)