summaryrefslogtreecommitdiffstats
path: root/html/src/components/terminal/xterm/addons
diff options
context:
space:
mode:
Diffstat (limited to 'html/src/components/terminal/xterm/addons')
-rw-r--r--html/src/components/terminal/xterm/addons/overlay.ts73
-rw-r--r--html/src/components/terminal/xterm/addons/zmodem.ts164
2 files changed, 237 insertions, 0 deletions
diff --git a/html/src/components/terminal/xterm/addons/overlay.ts b/html/src/components/terminal/xterm/addons/overlay.ts
new file mode 100644
index 0000000..6fa5a92
--- /dev/null
+++ b/html/src/components/terminal/xterm/addons/overlay.ts
@@ -0,0 +1,73 @@
+// ported from hterm.Terminal.prototype.showOverlay
+// https://chromium.googlesource.com/apps/libapps/+/master/hterm/js/hterm_terminal.js
+import { bind } from 'decko';
+import { ITerminalAddon, Terminal } from 'xterm';
+
+export class OverlayAddon implements ITerminalAddon {
+ private terminal: Terminal;
+ private overlayNode: HTMLElement;
+ private overlayTimeout?: number;
+
+ constructor() {
+ this.overlayNode = document.createElement('div');
+ this.overlayNode.style.cssText = `border-radius: 15px;
+font-size: xx-large;
+opacity: 0.75;
+padding: 0.2em 0.5em 0.2em 0.5em;
+position: absolute;
+-webkit-user-select: none;
+-webkit-transition: opacity 180ms ease-in;
+-moz-user-select: none;
+-moz-transition: opacity 180ms ease-in;`;
+
+ this.overlayNode.addEventListener(
+ 'mousedown',
+ e => {
+ e.preventDefault();
+ e.stopPropagation();
+ },
+ true
+ );
+ }
+
+ activate(terminal: Terminal): void {
+ this.terminal = terminal;
+ }
+
+ dispose(): void {}
+
+ @bind
+ showOverlay(msg: string, timeout?: number): void {
+ const { terminal, overlayNode } = this;
+ if (!terminal.element) return;
+
+ overlayNode.style.color = '#101010';
+ overlayNode.style.backgroundColor = '#f0f0f0';
+ overlayNode.textContent = msg;
+ overlayNode.style.opacity = '0.75';
+
+ if (!overlayNode.parentNode) {
+ terminal.element.appendChild(overlayNode);
+ }
+
+ const divSize = terminal.element.getBoundingClientRect();
+ const overlaySize = overlayNode.getBoundingClientRect();
+
+ overlayNode.style.top = (divSize.height - overlaySize.height) / 2 + 'px';
+ overlayNode.style.left = (divSize.width - overlaySize.width) / 2 + 'px';
+
+ if (this.overlayTimeout) clearTimeout(this.overlayTimeout);
+ if (!timeout) return;
+
+ this.overlayTimeout = window.setTimeout(() => {
+ overlayNode.style.opacity = '0';
+ this.overlayTimeout = window.setTimeout(() => {
+ if (overlayNode.parentNode) {
+ overlayNode.parentNode.removeChild(overlayNode);
+ }
+ this.overlayTimeout = undefined;
+ overlayNode.style.opacity = '0.75';
+ }, 200);
+ }, timeout || 1500);
+ }
+}
diff --git a/html/src/components/terminal/xterm/addons/zmodem.ts b/html/src/components/terminal/xterm/addons/zmodem.ts
new file mode 100644
index 0000000..3d38c5c
--- /dev/null
+++ b/html/src/components/terminal/xterm/addons/zmodem.ts
@@ -0,0 +1,164 @@
+import { bind } from 'decko';
+import { saveAs } from 'file-saver';
+import { IDisposable, ITerminalAddon, Terminal } from 'xterm';
+import * as Zmodem from 'zmodem.js/src/zmodem_browser';
+import { TrzszFilter } from 'trzsz';
+
+export interface ZmodeOptions {
+ zmodem: boolean;
+ trzsz: boolean;
+ onSend: () => void;
+ sender: (data: string | Uint8Array) => void;
+ writer: (data: string | Uint8Array) => void;
+}
+
+export class ZmodemAddon implements ITerminalAddon {
+ private disposables: IDisposable[] = [];
+ private terminal: Terminal;
+ private sentry: Zmodem.Sentry;
+ private session: Zmodem.Session;
+ private denier: () => void;
+ private trzszFilter: TrzszFilter;
+
+ constructor(private options: ZmodeOptions) {}
+
+ activate(terminal: Terminal) {
+ this.terminal = terminal;
+ if (this.options.zmodem) this.zmodemInit();
+ if (this.options.trzsz) this.trzszInit();
+ }
+
+ dispose() {
+ for (const d of this.disposables) {
+ d.dispose();
+ }
+ this.disposables.length = 0;
+ }
+
+ consume(data: ArrayBuffer) {
+ try {
+ if (this.options.trzsz) {
+ this.trzszFilter.processServerOutput(data);
+ } else {
+ this.sentry.consume(data);
+ }
+ } catch (e) {
+ console.error('[ttyd] zmodem consume: ', e);
+ this.reset();
+ }
+ }
+
+ @bind
+ private reset() {
+ this.terminal.options.disableStdin = false;
+ this.terminal.focus();
+ }
+
+ @bind
+ private trzszInit() {
+ const { terminal } = this;
+ const { sender, writer, zmodem } = this.options;
+ this.trzszFilter = new TrzszFilter({
+ writeToTerminal: data => {
+ if (!this.trzszFilter.isTransferringFiles() && zmodem) {
+ this.sentry.consume(data);
+ } else {
+ writer(typeof data === 'string' ? data : new Uint8Array(data as ArrayBuffer));
+ }
+ },
+ sendToServer: data => sender(data),
+ terminalColumns: terminal.cols,
+ });
+ this.disposables.push(terminal.onResize(size => this.trzszFilter.setTerminalColumns(size.cols)));
+ }
+
+ @bind
+ private zmodemInit() {
+ const { sender, writer } = this.options;
+ const { terminal, reset, zmodemDetect } = this;
+ this.session = null;
+ this.sentry = new Zmodem.Sentry({
+ to_terminal: octets => writer(new Uint8Array(octets)),
+ sender: octets => sender(new Uint8Array(octets)),
+ on_retract: () => reset(),
+ on_detect: detection => zmodemDetect(detection),
+ });
+ this.disposables.push(
+ terminal.onKey(e => {
+ const event = e.domEvent;
+ if (event.ctrlKey && event.key === 'c') {
+ if (this.denier) this.denier();
+ }
+ })
+ );
+ }
+
+ @bind
+ private zmodemDetect(detection: Zmodem.Detection): void {
+ const { terminal, receiveFile } = this;
+ terminal.options.disableStdin = true;
+
+ this.denier = () => detection.deny();
+ this.session = detection.confirm();
+ this.session.on('session_end', () => this.reset());
+
+ if (this.session.type === 'send') {
+ this.options.onSend();
+ } else {
+ receiveFile();
+ }
+ }
+
+ @bind
+ public sendFile(files: FileList) {
+ const { session, writeProgress } = this;
+ Zmodem.Browser.send_files(session, files, {
+ on_progress: (_, offer) => writeProgress(offer),
+ })
+ .then(() => session.close())
+ .catch(() => this.reset());
+ }
+
+ @bind
+ private receiveFile() {
+ const { session, writeProgress } = this;
+
+ session.on('offer', offer => {
+ offer.on('input', () => writeProgress(offer));
+ offer
+ .accept()
+ .then(payloads => {
+ const blob = new Blob(payloads, { type: 'application/octet-stream' });
+ saveAs(blob, offer.get_details().name);
+ })
+ .catch(() => this.reset());
+ });
+
+ session.start();
+ }
+
+ @bind
+ private writeProgress(offer: Zmodem.Offer) {
+ const { bytesHuman } = this;
+ const file = offer.get_details();
+ const name = file.name;
+ const size = file.size;
+ const offset = offer.get_offset();
+ const percent = ((100 * offset) / size).toFixed(2);
+
+ this.options.writer(`${name} ${percent}% ${bytesHuman(offset, 2)}/${bytesHuman(size, 2)}\r`);
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ 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]}`;
+ }
+}