Skip to content

Commit 84ac40a

Browse files
committed
html: move zmodem code as Component/ITerminalAddon
1 parent c4b9b6e commit 84ac40a

File tree

3 files changed

+172
-133
lines changed

3 files changed

+172
-133
lines changed

html/src/components/terminal/index.tsx

Lines changed: 9 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { Component, h } from 'preact';
33
import { ITerminalOptions, Terminal } from 'xterm';
44
import { FitAddon } from 'xterm-addon-fit';
55
import { WebLinksAddon } from 'xterm-addon-web-links';
6-
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
76

87
import { OverlayAddon } from './overlay';
9-
import { Modal } from '../modal';
8+
import { ZmodemAddon } from '../zmodem';
109

1110
import 'xterm/dist/xterm.css';
1211

@@ -34,24 +33,18 @@ interface Props {
3433
options: ITerminalOptions;
3534
}
3635

37-
interface State {
38-
modal: boolean;
39-
}
40-
41-
export class Xterm extends Component<Props, State> {
36+
export class Xterm extends Component<Props> {
4237
private textEncoder: TextEncoder;
4338
private textDecoder: TextDecoder;
4439
private container: HTMLElement;
4540
private terminal: Terminal;
4641
private fitAddon: FitAddon;
4742
private overlayAddon: OverlayAddon;
43+
private zmodemAddon: ZmodemAddon;
4844
private socket: WebSocket;
4945
private title: string;
5046
private reconnect: number;
5147
private resizeTimeout: number;
52-
private sentry: Zmodem.Sentry;
53-
private session: Zmodem.Session;
54-
private detection: Zmodem.Detection;
5548

5649
constructor(props) {
5750
super(props);
@@ -60,12 +53,6 @@ export class Xterm extends Component<Props, State> {
6053
this.textDecoder = new TextDecoder();
6154
this.fitAddon = new FitAddon();
6255
this.overlayAddon = new OverlayAddon();
63-
this.sentry = new Zmodem.Sentry({
64-
to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets),
65-
sender: (octets: ArrayLike<number>) => this.zmodemSend(octets),
66-
on_retract: () => {},
67-
on_detect: (detection: any) => this.zmodemDetect(detection),
68-
});
6956
}
7057

7158
componentDidMount() {
@@ -80,113 +67,23 @@ export class Xterm extends Component<Props, State> {
8067
window.removeEventListener('beforeunload', this.onWindowUnload);
8168
}
8269

83-
render({ id }: Props, { modal }: State) {
70+
render({ id }: Props) {
8471
return (
8572
<div id={id} ref={c => (this.container = c)}>
86-
<Modal show={modal}>
87-
<label class="file-label">
88-
<input
89-
onChange={this.sendFile}
90-
class="file-input"
91-
type="file"
92-
multiple
93-
/>
94-
<span class="file-cta">
95-
<strong>Choose files…</strong>
96-
</span>
97-
</label>
98-
</Modal>
73+
<ZmodemAddon ref={c => (this.zmodemAddon = c)} sender={this.sendData} />
9974
</div>
10075
);
10176
}
10277

10378
@bind
104-
private zmodemWrite(data: ArrayBuffer): void {
105-
const { terminal } = this;
106-
terminal.writeUtf8(new Uint8Array(data));
107-
}
108-
109-
@bind
110-
private zmodemSend(data: ArrayLike<number>): void {
79+
private sendData(data: ArrayLike<number>) {
11180
const { socket } = this;
11281
const payload = new Uint8Array(data.length + 1);
11382
payload[0] = Command.INPUT.charCodeAt(0);
11483
payload.set(data, 1);
11584
socket.send(payload);
11685
}
11786

118-
@bind
119-
private zmodemDetect(detection: Zmodem.Detection): void {
120-
const { terminal, receiveFile } = this;
121-
terminal.setOption('disableStdin', true);
122-
this.detection = detection;
123-
this.session = detection.confirm();
124-
125-
if (this.session.type === 'send') {
126-
this.setState({ modal: true });
127-
} else {
128-
receiveFile();
129-
}
130-
}
131-
132-
@bind
133-
private sendFile(event: Event) {
134-
this.setState({ modal: false });
135-
136-
const { terminal, session, writeProgress } = this;
137-
const files: FileList = (event.target as HTMLInputElement).files;
138-
139-
Zmodem.Browser.send_files(session, files, {
140-
on_progress: (_, xfer: any) => writeProgress(xfer),
141-
})
142-
.then(() => {
143-
session.close();
144-
this.detection = null;
145-
terminal.setOption('disableStdin', false);
146-
})
147-
.catch(e => {
148-
console.log(`[ttyd] zmodem send: `, e);
149-
});
150-
}
151-
152-
@bind
153-
private receiveFile() {
154-
const { terminal, session, writeProgress } = this;
155-
156-
session.on('offer', (xfer: any) => {
157-
const fileBuffer = [];
158-
xfer.on('input', payload => {
159-
writeProgress(xfer);
160-
fileBuffer.push(new Uint8Array(payload));
161-
});
162-
xfer.accept().then(() => {
163-
Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name);
164-
});
165-
});
166-
167-
session.on('session_end', () => {
168-
this.detection = null;
169-
terminal.setOption('disableStdin', false);
170-
});
171-
172-
session.start();
173-
}
174-
175-
@bind
176-
private writeProgress(xfer: any) {
177-
const { terminal, bytesHuman } = this;
178-
179-
const file = xfer.get_details();
180-
const name = file.name;
181-
const size = file.size;
182-
const offset = xfer.get_offset();
183-
const percent = ((100 * offset) / size).toFixed(2);
184-
185-
terminal.write(
186-
`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`
187-
);
188-
}
189-
19087
@bind
19188
private onWindowResize() {
19289
const { fitAddon } = this;
@@ -219,6 +116,7 @@ export class Xterm extends Component<Props, State> {
219116
terminal.loadAddon(fitAddon);
220117
terminal.loadAddon(overlayAddon);
221118
terminal.loadAddon(new WebLinksAddon());
119+
terminal.loadAddon(this.zmodemAddon);
222120

223121
terminal.onTitleChange(data => {
224122
if (data && data !== '') {
@@ -273,23 +171,14 @@ export class Xterm extends Component<Props, State> {
273171

274172
@bind
275173
private onSocketData(event: MessageEvent) {
276-
const { terminal, textDecoder } = this;
174+
const { terminal, textDecoder, zmodemAddon } = this;
277175
const rawData = event.data as ArrayBuffer;
278176
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]);
279177
const data = rawData.slice(1);
280178

281179
switch (cmd) {
282180
case Command.OUTPUT:
283-
try {
284-
this.sentry.consume(data);
285-
} catch (e) {
286-
console.log(`[ttyd] zmodem consume: `, e);
287-
terminal.setOption('disableStdin', false);
288-
if (this.detection) {
289-
this.detection.deny();
290-
this.detection = null;
291-
}
292-
}
181+
zmodemAddon.consume(data);
293182
break;
294183
case Command.SET_WINDOW_TITLE:
295184
this.title = textDecoder.decode(data);
@@ -331,16 +220,4 @@ export class Xterm extends Component<Props, State> {
331220
socket.send(textEncoder.encode(Command.INPUT + data));
332221
}
333222
}
334-
335-
private bytesHuman(bytes: any, precision: number): string {
336-
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
337-
return '-';
338-
}
339-
if (bytes === 0) return '0';
340-
if (typeof precision === 'undefined') precision = 1;
341-
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
342-
const num = Math.floor(Math.log(bytes) / Math.log(1024));
343-
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
344-
return `${value} ${units[num]}`;
345-
}
346223
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { bind } from 'decko';
2+
import { Component, h } from 'preact';
3+
import { ITerminalAddon, Terminal } from 'xterm';
4+
import * as Zmodem from 'zmodem.js/src/zmodem_browser';
5+
6+
import { Modal } from '../modal';
7+
8+
interface Props {
9+
sender: (data: ArrayLike<number>) => void;
10+
}
11+
12+
interface State {
13+
modal: boolean;
14+
}
15+
16+
export class ZmodemAddon extends Component<Props, State>
17+
implements ITerminalAddon {
18+
private terminal: Terminal | undefined;
19+
private sentry: Zmodem.Sentry;
20+
private session: Zmodem.Session;
21+
22+
constructor(props) {
23+
super(props);
24+
25+
this.sentry = new Zmodem.Sentry({
26+
to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets),
27+
sender: (octets: ArrayLike<number>) => this.zmodemSend(octets),
28+
on_retract: () => this.zmodemRetract(),
29+
on_detect: (detection: any) => this.zmodemDetect(detection),
30+
});
31+
}
32+
33+
render(_, { modal }: State) {
34+
return (
35+
<Modal show={modal}>
36+
<label class="file-label">
37+
<input
38+
onChange={this.sendFile}
39+
class="file-input"
40+
type="file"
41+
multiple
42+
/>
43+
<span class="file-cta">
44+
<strong>Choose files…</strong>
45+
</span>
46+
</label>
47+
</Modal>
48+
);
49+
}
50+
51+
activate(terminal: Terminal): void {
52+
this.terminal = terminal;
53+
}
54+
55+
dispose(): void {}
56+
57+
consume(data: ArrayBuffer) {
58+
const { sentry, terminal } = this;
59+
try {
60+
sentry.consume(data);
61+
} catch (e) {
62+
console.log(`[ttyd] zmodem consume: `, e);
63+
terminal.setOption('disableStdin', false);
64+
}
65+
}
66+
67+
@bind
68+
private zmodemWrite(data: ArrayBuffer): void {
69+
this.terminal.writeUtf8(new Uint8Array(data));
70+
}
71+
72+
@bind
73+
private zmodemSend(data: ArrayLike<number>): void {
74+
this.props.sender(data);
75+
}
76+
77+
@bind
78+
private zmodemRetract(): void {
79+
this.terminal.setOption('disableStdin', false);
80+
}
81+
82+
@bind
83+
private zmodemDetect(detection: Zmodem.Detection): void {
84+
const { terminal, receiveFile } = this;
85+
terminal.setOption('disableStdin', true);
86+
this.session = detection.confirm();
87+
88+
if (this.session.type === 'send') {
89+
this.setState({ modal: true });
90+
} else {
91+
receiveFile();
92+
}
93+
}
94+
95+
@bind
96+
private sendFile(event: Event) {
97+
this.setState({ modal: false });
98+
99+
const { terminal, session, writeProgress } = this;
100+
const files: FileList = (event.target as HTMLInputElement).files;
101+
102+
Zmodem.Browser.send_files(session, files, {
103+
on_progress: (_, xfer: any) => writeProgress(xfer),
104+
})
105+
.then(() => {
106+
session.close();
107+
terminal.setOption('disableStdin', false);
108+
})
109+
.catch(e => {
110+
console.log(`[ttyd] zmodem send: `, e);
111+
});
112+
}
113+
114+
@bind
115+
private receiveFile() {
116+
const { terminal, session, writeProgress } = this;
117+
118+
session.on('offer', (xfer: any) => {
119+
const fileBuffer = [];
120+
xfer.on('input', payload => {
121+
writeProgress(xfer);
122+
fileBuffer.push(new Uint8Array(payload));
123+
});
124+
xfer.accept().then(() => {
125+
Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name);
126+
});
127+
});
128+
129+
session.on('session_end', () => {
130+
terminal.setOption('disableStdin', false);
131+
});
132+
133+
session.start();
134+
}
135+
136+
@bind
137+
private writeProgress(xfer: any) {
138+
const { terminal, bytesHuman } = this;
139+
140+
const file = xfer.get_details();
141+
const name = file.name;
142+
const size = file.size;
143+
const offset = xfer.get_offset();
144+
const percent = ((100 * offset) / size).toFixed(2);
145+
146+
terminal.write(
147+
`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`
148+
);
149+
}
150+
151+
private bytesHuman(bytes: any, precision: number): string {
152+
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
153+
return '-';
154+
}
155+
if (bytes === 0) return '0';
156+
if (typeof precision === 'undefined') precision = 1;
157+
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
158+
const num = Math.floor(Math.log(bytes) / Math.log(1024));
159+
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
160+
return `${value} ${units[num]}`;
161+
}
162+
}

src/index.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)