Skip to content

Commit

Permalink
html: add zmodem support back
Browse files Browse the repository at this point in the history
  • Loading branch information
tsl0922 committed Jun 23, 2019
1 parent 86123ca commit 6a40f32
Show file tree
Hide file tree
Showing 6 changed files with 278 additions and 13 deletions.
3 changes: 2 additions & 1 deletion html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"preact": "^8.4.2",
"xterm": "^3.14.2",
"xterm-addon-fit": "^0.1.0-beta1",
"xterm-addon-web-links": "^0.1.0-beta10"
"xterm-addon-web-links": "^0.1.0-beta10",
"zmodem.js": "^0.1.9"
}
}
27 changes: 27 additions & 0 deletions html/src/components/modal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Component, ComponentChildren, h } from 'preact';

import './modal.scss';

interface Props {
show: boolean;
children: ComponentChildren;
}

export class Modal extends Component<Props> {
constructor(props) {
super(props);
}

render({ show, children }: Props) {
return (
show && (
<div className="modal">
<div className="modal-background" />
<div className="modal-content">
<div className="box">{children}</div>
</div>
</div>
)
);
}
}
81 changes: 81 additions & 0 deletions html/src/components/modal/modal.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.modal {
bottom: 0;
left: 0;
right: 0;
top: 0;
align-items: center;
display: flex;
overflow: hidden;
position: fixed;
z-index: 40;
}

.modal-background {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
background-color: hsla(0, 0%, 4%, 0.86);
}

.modal-content {
margin: 0 20px;
max-height: calc(100vh - 160px);
overflow: auto;
position: relative;
width: 100%;

.box {
background-color: #fff;
border-radius: 5px;
box-shadow: 0 2px 3px hsla(0, 0%, 4%, 0.1), 0 0 0 1px hsla(0, 0%, 4%, 0.1);
color: #4a4a4a;
display: block;
padding: 1.25rem;
}

header {
font-weight: bold;
text-align: center;
padding-bottom: 10px;
margin-bottom: 10px;
border-bottom: 1px solid #ddd;
}

.file-input {
height: .01em;
left: 0;
outline: none;
position: absolute;
top: 0;
width: .01em;
}

.file-cta {
background-color: #f5f5f5;
color: #4a4a4a;
outline: none;
align-items: center;
box-shadow: none;
display: inline-flex;
height: 2.25em;
justify-content: flex-start;
line-height: 1.5;
position: relative;
vertical-align: top;
border-color: #dbdbdb;
border-radius: 3px;
font-size: 1em;
padding: calc(.375em - 1px) 1em;
white-space: nowrap;
}
}

@media print, screen and (min-width: 769px) {
.modal-content {
margin: 0 auto;
max-height: calc(100vh - 40px);
width: 640px;
}
}
153 changes: 142 additions & 11 deletions html/src/components/terminal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { Component, h } from 'preact';
import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { WebLinksAddon } from 'xterm-addon-web-links';
import * as Zmodem from 'zmodem.js/src/zmodem_browser';

import { OverlayAddon } from './overlay';
import { Modal } from '../modal';

import 'xterm/dist/xterm.css';

Expand Down Expand Up @@ -31,7 +34,11 @@ interface Props {
options: ITerminalOptions;
}

export class Xterm extends Component<Props> {
interface State {
modal: boolean;
}

export class Xterm extends Component<Props, State> {
private textEncoder: TextEncoder;
private textDecoder: TextDecoder;
private container: HTMLElement;
Expand All @@ -42,6 +49,8 @@ export class Xterm extends Component<Props> {
private title: string;
private autoReconnect: number;
private resizeTimeout: number;
private sentry: Zmodem.Sentry;
private session: Zmodem.Session;

constructor(props) {
super(props);
Expand All @@ -50,6 +59,12 @@ export class Xterm extends Component<Props> {
this.textDecoder = new TextDecoder();
this.fitAddon = new FitAddon();
this.overlayAddon = new OverlayAddon();
this.sentry = new Zmodem.Sentry({
to_terminal: (octets: ArrayBuffer) => this.zmodemWrite(octets),
sender: (octets: number[]) => this.zmodemSend(octets),
on_retract: () => {},
on_detect: (detection: any) => this.zmodemDetect(detection),
});
}

componentDidMount() {
Expand All @@ -64,8 +79,107 @@ export class Xterm extends Component<Props> {
window.removeEventListener('beforeunload', this.onWindowUnload);
}

render({ id }: Props) {
return <div id={id} ref={c => (this.container = c)} />;
render({ id }: Props, { modal }: State) {
return (
<div id={id} ref={c => (this.container = c)}>
<Modal show={modal}>
<label class="file-label">
<input
onChange={this.sendFile}
class="file-input"
type="file"
multiple
/>
<span class="file-cta">
<strong>Choose files…</strong>
</span>
</label>
</Modal>
</div>
);
}

@bind
private zmodemWrite(data: ArrayBuffer): void {
const { terminal } = this;
terminal.writeUtf8(new Uint8Array(data));
}

@bind
private zmodemSend(data: number[]): void {
const { socket } = this;
const buffer = new Uint8Array(data.length + 1);
buffer[0] = Command.INPUT.charCodeAt(0);
buffer.set(data, 1);
socket.send(buffer);
}

@bind
private zmodemDetect(detection: any): void {
const { terminal, receiveFile } = this;

terminal.setOption('disableStdin', true);
this.session = detection.confirm();
if (this.session.type === 'send') {
this.setState({ modal: true });
} else {
receiveFile();
}
}

@bind
private sendFile(event: Event) {
this.setState({ modal: false });

const { terminal, session, writeProgress } = this;
const files: FileList = (event.target as HTMLInputElement).files;
if (files.length === 0) {
session.close();
terminal.setOption('disableStdin', false);
return;
}

Zmodem.Browser.send_files(session, files, {
on_progress: (_, xfer) => writeProgress(xfer),
on_file_complete: () => {},
}).then(() => {
session.close();
terminal.setOption('disableStdin', false);
});
}

@bind
private receiveFile() {
const { terminal, session, writeProgress } = this;

session.on('offer', (xfer: any) => {
const fileBuffer = [];
xfer.on('input', payload => {
writeProgress(xfer);
fileBuffer.push(new Uint8Array(payload));
});
xfer.accept().then(() => {
Zmodem.Browser.save_to_disk(fileBuffer, xfer.get_details().name);
terminal.setOption('disableStdin', false);
});
});

session.start();
}

@bind
private writeProgress(xfer: any) {
const { terminal, bytesHuman } = this;

const file = xfer.get_details();
const name = file.name;
const size = file.size;
const offset = xfer.get_offset();
const percent = ((100 * offset) / size).toFixed(2);

terminal.write(
`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`
);
}

@bind
Expand Down Expand Up @@ -154,29 +268,34 @@ export class Xterm extends Component<Props> {

@bind
private onSocketData(event: MessageEvent) {
const { terminal, textDecoder } = this;

const rawData = new Uint8Array(event.data);
const cmd = String.fromCharCode(rawData[0]);
const { terminal, textDecoder, socket, openTerminal } = this;
const rawData = event.data as ArrayBuffer;
const cmd = String.fromCharCode(new Uint8Array(rawData)[0]);
const data = rawData.slice(1);

switch (cmd) {
case Command.OUTPUT:
terminal.writeUtf8(data);
try {
this.sentry.consume(data);
} catch (e) {
console.log(`[ttyd] zmodem consume: `, e);
socket.close();
setTimeout(() => openTerminal(), 500);
}
break;
case Command.SET_WINDOW_TITLE:
this.title = textDecoder.decode(data.buffer);
this.title = textDecoder.decode(data);
document.title = this.title;
break;
case Command.SET_PREFERENCES:
const preferences = JSON.parse(textDecoder.decode(data.buffer));
const preferences = JSON.parse(textDecoder.decode(data));
Object.keys(preferences).forEach(key => {
console.log(`[ttyd] setting ${key}: ${preferences[key]}`);
terminal.setOption(key, preferences[key]);
});
break;
case Command.SET_RECONNECT:
this.autoReconnect = Number(textDecoder.decode(data.buffer));
this.autoReconnect = Number(textDecoder.decode(data));
console.log(`[ttyd] enabling reconnect: ${this.autoReconnect} seconds`);
break;
default:
Expand Down Expand Up @@ -204,4 +323,16 @@ export class Xterm extends Component<Props> {
socket.send(textEncoder.encode(Command.INPUT + data));
}
}

private bytesHuman(bytes: any, precision: number): string {
if (!/^([-+])?|(\.\d+)(\d+(\.\d+)?|(\d+\.)|Infinity)$/.test(bytes)) {
return '-';
}
if (bytes === 0) return '0';
if (typeof precision === 'undefined') precision = 1;
const units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const num = Math.floor(Math.log(bytes) / Math.log(1024));
const value = (bytes / Math.pow(1024, Math.floor(num))).toFixed(precision);
return `${value} ${units[num]}`;
}
}
25 changes: 25 additions & 0 deletions html/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1601,6 +1601,14 @@ cosmiconfig@^5.0.0, cosmiconfig@^5.2.0:
js-yaml "^3.13.1"
parse-json "^4.0.0"

crc-32@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
dependencies:
exit-on-epipe "~1.0.1"
printj "~1.1.0"

create-ecdh@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff"
Expand Down Expand Up @@ -2519,6 +2527,11 @@ execa@^1.0.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"

exit-on-epipe@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==

expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
Expand Down Expand Up @@ -6643,6 +6656,11 @@ pretty-hrtime@^1.0.0:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=

printj@~1.1.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==

process-nextick-args@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3"
Expand Down Expand Up @@ -9164,3 +9182,10 @@ yup@^0.27.0:
property-expr "^1.5.0"
synchronous-promise "^2.0.6"
toposort "^2.0.2"

zmodem.js@^0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/zmodem.js/-/zmodem.js-0.1.9.tgz#8dda36d45091bbdf263819f961d3c1a20223daf7"
integrity sha512-xixLjW1eML0uiWULsXDInyfwNW9mqESzz7ra+2MWHNG2F5JINEkE5vzF5MigpPcLvrYoHdnehPcJwQZlDph3hQ==
dependencies:
crc-32 "^1.1.1"
2 changes: 1 addition & 1 deletion src/index.html

Large diffs are not rendered by default.

0 comments on commit 6a40f32

Please sign in to comment.