first commit
This commit is contained in:
29
xterminal/source/base/debouncer.ts
Normal file
29
xterminal/source/base/debouncer.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { isFunction } from "../helpers";
|
||||
|
||||
/**
|
||||
* Debouncing functions
|
||||
*
|
||||
* https://www.freecodecamp.org/news/javascript-debounce-example
|
||||
*
|
||||
* https://programmingwithmosh.com/javascript/javascript/throttle-and-debounce-patterns/
|
||||
*/
|
||||
|
||||
const DEBOUNCE_TIME = 0;
|
||||
|
||||
export function bounce(fn: TimerHandler, ...args: unknown[]): number {
|
||||
return setTimeout(fn, DEBOUNCE_TIME, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay the execution of the function until a pause happens
|
||||
* @param fn The function to execute
|
||||
* @returns A function that limits the intermediate calls to `fn`
|
||||
*/
|
||||
export function debounce(fn: TimerHandler) {
|
||||
let flag: number;
|
||||
return (...args: unknown[]) => {
|
||||
if (!isFunction(fn)) return;
|
||||
clearTimeout(flag);
|
||||
flag = bounce(fn, ...args);
|
||||
};
|
||||
}
|
||||
42
xterminal/source/base/disposable.ts
Normal file
42
xterminal/source/base/disposable.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { IDisposable } from "../types";
|
||||
|
||||
/**
|
||||
* Disposables
|
||||
*
|
||||
* https://blog.hediet.de/post/the_disposable_pattern_in_typescript
|
||||
*
|
||||
* https://github.com/xtermjs/xterm.js/blob/master/src/common/Lifecycle.ts
|
||||
*/
|
||||
|
||||
export default class Disposable implements IDisposable {
|
||||
// private store for states
|
||||
#disposables: IDisposable[];
|
||||
|
||||
public isDisposed: boolean;
|
||||
|
||||
constructor() {
|
||||
this.isDisposed = false;
|
||||
this.#disposables = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a disposable object
|
||||
* @param d The disposable to register
|
||||
*/
|
||||
public register<T extends IDisposable>(d: T): void {
|
||||
if (this.isDisposed) {
|
||||
d?.dispose();
|
||||
} else {
|
||||
this.#disposables.push(d);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the object, triggering the `dispose` method on all registered disposables
|
||||
*/
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) return;
|
||||
this.isDisposed = true;
|
||||
this.#disposables.forEach((d) => d?.dispose());
|
||||
}
|
||||
}
|
||||
13
xterminal/source/base/error.ts
Normal file
13
xterminal/source/base/error.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class XError extends Error {
|
||||
constructor(message: string) {
|
||||
message = "[x] " + message;
|
||||
super(message);
|
||||
this.name = "XTerminalError";
|
||||
}
|
||||
}
|
||||
|
||||
export const TARGET_INVALID_ERR =
|
||||
"mount: A parent HTMLElement (target) is required";
|
||||
|
||||
export const TARGET_NOT_CONNECTED_ERR =
|
||||
"'mount' was called on an HTMLElement (target) that is not attached to DOM.";
|
||||
70
xterminal/source/base/reactivity.ts
Normal file
70
xterminal/source/base/reactivity.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { isFunction } from "../helpers";
|
||||
import type { IDisposable } from "../types";
|
||||
|
||||
/**
|
||||
* Reactivity
|
||||
* => https://github.com/henryhale/reactivity
|
||||
*/
|
||||
|
||||
/**
|
||||
* Effect - callback triggered when a reactive value changes
|
||||
*/
|
||||
export type IEffect = () => void;
|
||||
|
||||
/**
|
||||
* Reactive value -> all effects are disposable
|
||||
*/
|
||||
export interface IReactive<T> extends IDisposable {
|
||||
value: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global observer
|
||||
*/
|
||||
let observer: IEffect | null;
|
||||
|
||||
/**
|
||||
* Function that creates a disposable reactive object from a primitive value
|
||||
* @param value The primitive value (initial)
|
||||
* @returns Reactive object
|
||||
*/
|
||||
export function ref<T>(value: T): IReactive<T> {
|
||||
const observers = new Set<IEffect>();
|
||||
let disposed = false;
|
||||
return {
|
||||
get value() {
|
||||
// allow subscriptons only when the object is not disposed
|
||||
if (!disposed && isFunction(observer)) {
|
||||
observers.add(observer);
|
||||
}
|
||||
return value;
|
||||
},
|
||||
set value(newValue) {
|
||||
value = newValue;
|
||||
// alert subscribers only when the object is not disposed
|
||||
if (!disposed) {
|
||||
observers.forEach((o) => o.call(undefined));
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
// remove all subscriptions
|
||||
observers.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Function that opens a subscription to reactive values
|
||||
* @param fn The subscription (function) to be made
|
||||
*/
|
||||
export function createEffect(fn: IEffect): void {
|
||||
if (!isFunction(fn)) return;
|
||||
observer = fn;
|
||||
try {
|
||||
fn.call(undefined);
|
||||
} finally {
|
||||
observer = null;
|
||||
}
|
||||
}
|
||||
77
xterminal/source/emitter/index.ts
Normal file
77
xterminal/source/emitter/index.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import Disposable from "../base/disposable";
|
||||
import type {
|
||||
XEventEmitter as IEventEmitter,
|
||||
IEventListener,
|
||||
IEventName
|
||||
} from "../types";
|
||||
import type { IEmitterState } from "./interface";
|
||||
|
||||
/**
|
||||
* EventEmitter class
|
||||
*
|
||||
* https://nodejs.dev/api/events.html
|
||||
*
|
||||
* https://nodejs.dev/en/learn/the-nodejs-event-emitter/
|
||||
*/
|
||||
export default class XEventEmitter extends Disposable implements IEventEmitter {
|
||||
// private store for states
|
||||
#state: IEmitterState;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.#state = {
|
||||
stack: [],
|
||||
store: new Map()
|
||||
};
|
||||
this.register({ dispose: () => this.#state.store.clear() });
|
||||
}
|
||||
|
||||
public on(eventName: IEventName, listener: IEventListener): void {
|
||||
const store = this.#state.store;
|
||||
if (store.has(eventName)) {
|
||||
store.get(eventName)?.add(listener);
|
||||
} else {
|
||||
store.set(eventName, new Set([listener]));
|
||||
}
|
||||
}
|
||||
|
||||
public once(eventName: IEventName, listener: IEventListener): void {
|
||||
const evlistener = (...args: unknown[]) => {
|
||||
listener.call(undefined, ...args);
|
||||
this.off(eventName, evlistener);
|
||||
};
|
||||
const store = this.#state.store;
|
||||
if (store.has(eventName)) {
|
||||
store.get(eventName)?.add(evlistener);
|
||||
} else {
|
||||
store.set(eventName, new Set([evlistener]));
|
||||
}
|
||||
}
|
||||
|
||||
public off(eventName: IEventName, listener: IEventListener): void {
|
||||
const all = this.#state.store.get(eventName);
|
||||
if (all) {
|
||||
for (const fn of all) {
|
||||
if (fn === listener) {
|
||||
all.delete(listener);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public emit(eventName: IEventName, ...args: unknown[]): void {
|
||||
if (this.isDisposed) return;
|
||||
const stack = this.#state.stack;
|
||||
if (stack.includes(eventName)) return;
|
||||
const listeners = this.#state.store.get(eventName);
|
||||
if (listeners) {
|
||||
stack.push(eventName);
|
||||
for (const fn of listeners) {
|
||||
fn.call(undefined, ...args);
|
||||
}
|
||||
stack.pop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
9
xterminal/source/emitter/interface.ts
Normal file
9
xterminal/source/emitter/interface.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { IEventName, IEventListener } from "../types";
|
||||
|
||||
/**
|
||||
* State of the Event Emitter
|
||||
*/
|
||||
export type IEmitterState = {
|
||||
store: Map<IEventName, Set<IEventListener>>;
|
||||
stack: IEventName[];
|
||||
};
|
||||
28
xterminal/source/helpers.ts
Normal file
28
xterminal/source/helpers.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const isArray = Array.isArray;
|
||||
|
||||
export function isObject(val: unknown): val is object {
|
||||
return typeof val === "object" && val !== null;
|
||||
}
|
||||
|
||||
export function isFunction(val: unknown): val is (...a: unknown[]) => unknown {
|
||||
return typeof val === "function";
|
||||
}
|
||||
|
||||
/**
|
||||
* Detecting a mobile browser
|
||||
* https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
|
||||
*/
|
||||
|
||||
// TODO: compatibility check
|
||||
export function isMobile(): boolean {
|
||||
const _window = window || {};
|
||||
const _navigator = navigator || {};
|
||||
// Check for touch support
|
||||
if ("ontouchstart" in _window || _navigator.maxTouchPoints) {
|
||||
// Check for mobile user agent
|
||||
if (/Mobi/.test(_navigator.userAgent)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
46
xterminal/source/history/index.ts
Normal file
46
xterminal/source/history/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { isArray } from "../helpers";
|
||||
import type { IHistory } from "./interface";
|
||||
|
||||
/**
|
||||
* History stack
|
||||
*/
|
||||
export default class XHistory implements IHistory {
|
||||
private store;
|
||||
private ptr;
|
||||
|
||||
constructor(initialState: string[] = []) {
|
||||
this.store = isArray(initialState) ? initialState : [];
|
||||
this.ptr = -1;
|
||||
}
|
||||
|
||||
private get size(): number {
|
||||
return this.store.length;
|
||||
}
|
||||
|
||||
public get list(): string[] {
|
||||
return [].slice.call(this.store).reverse();
|
||||
}
|
||||
|
||||
add(input: string): void {
|
||||
if (input && input !== this.store[0]) {
|
||||
this.store.unshift(input);
|
||||
}
|
||||
this.ptr = -1;
|
||||
}
|
||||
|
||||
previous(): string {
|
||||
this.ptr++;
|
||||
if (this.ptr >= this.size) this.ptr = this.size - 1;
|
||||
return this.store[this.ptr] || "";
|
||||
}
|
||||
|
||||
next(): string {
|
||||
this.ptr--;
|
||||
if (this.ptr <= -1) this.ptr = -1;
|
||||
return this.store[this.ptr] || "";
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.store.splice(0);
|
||||
}
|
||||
}
|
||||
33
xterminal/source/history/interface.ts
Normal file
33
xterminal/source/history/interface.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Interface: History
|
||||
*/
|
||||
export interface IHistory {
|
||||
/**
|
||||
* Array containing a copy of entries
|
||||
*/
|
||||
list: string[];
|
||||
|
||||
/**
|
||||
* Getter: access one entry at a time (forward)
|
||||
*/
|
||||
next(): string;
|
||||
|
||||
/**
|
||||
* Getter: access one entry at a time (backwards)
|
||||
*/
|
||||
previous(): string;
|
||||
|
||||
/**
|
||||
* Insert an input string to the stack
|
||||
*
|
||||
* Returns `false` if the `input` is the same as the previous entry
|
||||
*
|
||||
* @returns boolean
|
||||
*/
|
||||
add(input: string): void;
|
||||
|
||||
/**
|
||||
* Empty the stack of entries
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
139
xterminal/source/index.ts
Normal file
139
xterminal/source/index.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import XEventEmitter from "./emitter/index";
|
||||
import { isFunction } from "./helpers";
|
||||
import type { ITerminalOptions } from "./types";
|
||||
import type { ITerminalState } from "./interface";
|
||||
import { CLEAR_EVENT, PAUSE_EVENT, RESUME_EVENT, setup } from "./instance";
|
||||
import {
|
||||
TARGET_INVALID_ERR,
|
||||
TARGET_NOT_CONNECTED_ERR,
|
||||
XError
|
||||
} from "./base/error";
|
||||
import { escapeHTML } from "./output/index";
|
||||
import { NEWLINE } from "./renderer/dom";
|
||||
|
||||
export default class XTerminal extends XEventEmitter {
|
||||
#state!: ITerminalState;
|
||||
|
||||
public isMounted: boolean;
|
||||
|
||||
constructor(options?: ITerminalOptions) {
|
||||
super();
|
||||
this.isMounted = false;
|
||||
if (options && options?.target) {
|
||||
this.mount(options.target);
|
||||
}
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.#state.input.focus();
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
this.#state.input.blur();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.#state.input.pause();
|
||||
this.emit(PAUSE_EVENT);
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.#state.input.resume();
|
||||
this.emit(RESUME_EVENT);
|
||||
}
|
||||
|
||||
public setInput(value: string): void {
|
||||
this.#state.input.setValue(value);
|
||||
}
|
||||
|
||||
public clearInput(): void {
|
||||
this.#state.input.clear();
|
||||
}
|
||||
|
||||
public write(data: string | number, callback?: () => void): void {
|
||||
this.#state.output.write("" + data, callback);
|
||||
}
|
||||
|
||||
public writeln(data: string | number, callback?: () => void): void {
|
||||
this.#state.output.write("" + data + NEWLINE, callback);
|
||||
}
|
||||
|
||||
public writeSafe(data: string | number, callback?: () => void): void {
|
||||
this.#state.output.writeSafe("" + data, callback);
|
||||
}
|
||||
|
||||
public writelnSafe(data: string | number, callback?: () => void): void {
|
||||
this.#state.output.writeSafe("" + data + NEWLINE, callback);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.#state.output.clear();
|
||||
this.emit(CLEAR_EVENT);
|
||||
}
|
||||
|
||||
public clearLast(): void {
|
||||
this.#state.output.clearLast();
|
||||
}
|
||||
|
||||
public get history(): string[] {
|
||||
return this.#state.history.list || [];
|
||||
}
|
||||
|
||||
public set history(newState: string[]) {
|
||||
newState.forEach((item) => this.#state.history.add(item));
|
||||
}
|
||||
|
||||
public clearHistory(): void {
|
||||
this.#state.history.clear();
|
||||
}
|
||||
|
||||
public setCompleter(fn: (data: string) => string): void {
|
||||
if (!isFunction(fn)) return;
|
||||
this.#state.completer = fn;
|
||||
}
|
||||
|
||||
public mount(target: HTMLElement | string): void {
|
||||
if (this.isMounted) return;
|
||||
|
||||
if (target && typeof target === "string") {
|
||||
target = document.querySelector<HTMLElement>(target) as HTMLElement;
|
||||
}
|
||||
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
throw new XError(TARGET_INVALID_ERR);
|
||||
}
|
||||
|
||||
if (!target.isConnected && console) {
|
||||
console.warn(TARGET_NOT_CONNECTED_ERR);
|
||||
}
|
||||
|
||||
target.innerHTML = "";
|
||||
|
||||
this.#state = setup(this, target);
|
||||
|
||||
this.isMounted = true;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
const state = this.#state;
|
||||
state.history.clear();
|
||||
state.completer = undefined;
|
||||
state.input.dispose();
|
||||
const box = state.output.el.parentNode;
|
||||
box?.parentNode?.removeChild(box);
|
||||
this.isMounted = false;
|
||||
}
|
||||
|
||||
static get version() {
|
||||
return "__VERSION__";
|
||||
}
|
||||
|
||||
static get XEventEmitter() {
|
||||
return XEventEmitter;
|
||||
}
|
||||
|
||||
static escapeHTML(data?: string): string {
|
||||
return escapeHTML(data);
|
||||
}
|
||||
}
|
||||
29
xterminal/source/input/cursor.ts
Normal file
29
xterminal/source/input/cursor.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { IReactive } from "../base/reactivity";
|
||||
|
||||
/**
|
||||
* Get current cursor position in a textbox
|
||||
* https://stackoverflow.com/16105482/get-current-cursor-position-in-a-textbox
|
||||
*/
|
||||
|
||||
// TODO: compatibility check
|
||||
function getCursorPosition<T extends HTMLElement>(field: T): number {
|
||||
if ("selectionStart" in field) {
|
||||
return field.selectionStart as number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function updateCursor(
|
||||
el: HTMLTextAreaElement,
|
||||
data: IReactive<string>,
|
||||
ptr: IReactive<number>
|
||||
) {
|
||||
let pos = getCursorPosition(el);
|
||||
const len = data.value.length;
|
||||
if (pos > len) {
|
||||
pos = len;
|
||||
} else if (pos < 0) {
|
||||
pos = 0;
|
||||
}
|
||||
ptr.value = pos;
|
||||
}
|
||||
169
xterminal/source/input/index.ts
Normal file
169
xterminal/source/input/index.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import Disposable from "../base/disposable";
|
||||
import type { IReactive } from "../base/reactivity";
|
||||
import { isFunction } from "../helpers";
|
||||
import { inputBuild } from "../renderer/index";
|
||||
import { SPACE, THEME, h } from "../renderer/dom";
|
||||
import { ENTER_KEY, addEvent } from "../renderer/events";
|
||||
import { updateCursor } from "./cursor";
|
||||
import { debounce } from "../base/debouncer";
|
||||
import { createEffect, ref } from "../base/reactivity";
|
||||
import type { IOutputInterface } from "../output/interface";
|
||||
import type { IInputInterface } from "./interface";
|
||||
import type { IKeyPress } from "../types";
|
||||
|
||||
/**
|
||||
* Input Component
|
||||
*/
|
||||
export default class XInputComponent
|
||||
extends Disposable
|
||||
implements IInputInterface
|
||||
{
|
||||
public readonly el: HTMLTextAreaElement;
|
||||
private buffer: string;
|
||||
private data: IReactive<string>;
|
||||
private ptr: IReactive<number>;
|
||||
private isActive: IReactive<boolean>;
|
||||
public showInput: IReactive<boolean>;
|
||||
public isFocused: IReactive<boolean>;
|
||||
public onkeypress?: ((ev: IKeyPress) => void) | undefined;
|
||||
|
||||
constructor(target: HTMLElement) {
|
||||
super();
|
||||
|
||||
this.el = inputBuild(target);
|
||||
this.buffer = "";
|
||||
this.data = ref<string>("");
|
||||
this.ptr = ref<number>(0);
|
||||
this.isActive = ref<boolean>(true);
|
||||
this.showInput = ref<boolean>(true);
|
||||
this.isFocused = ref<boolean>(false);
|
||||
|
||||
// All reactive values are disposable
|
||||
this.register(this.data);
|
||||
this.register(this.ptr);
|
||||
this.register(this.isActive);
|
||||
this.register(this.showInput);
|
||||
this.register(this.isFocused);
|
||||
|
||||
const cursorUpdater = () => updateCursor(this.el, this.data, this.ptr);
|
||||
|
||||
const cursorHandler = debounce(cursorUpdater);
|
||||
|
||||
const inputHandler = debounce(() => {
|
||||
this.data.value = this.buffer = this.el.value;
|
||||
});
|
||||
|
||||
createEffect(cursorUpdater);
|
||||
|
||||
this.register(
|
||||
addEvent(this.el, "blur", () => (this.isFocused.value = false))
|
||||
);
|
||||
|
||||
this.register(
|
||||
addEvent(
|
||||
this.el,
|
||||
"focus",
|
||||
() => (this.isFocused.value = true),
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
this.register(
|
||||
addEvent(
|
||||
this.el,
|
||||
"keyup",
|
||||
() => this.isActive.value && cursorHandler(),
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
this.register(
|
||||
addEvent(this.el, "input", () => {
|
||||
inputHandler();
|
||||
cursorHandler();
|
||||
})
|
||||
);
|
||||
|
||||
this.register(
|
||||
addEvent(this.el, "keydown", (ev: KeyboardEvent) => {
|
||||
ev.stopImmediatePropagation();
|
||||
const value = this.data.value;
|
||||
if (ev.key === ENTER_KEY) {
|
||||
ev.preventDefault();
|
||||
if (this.el) this.el.value = "";
|
||||
this.data.value = "";
|
||||
this.buffer = "";
|
||||
this.showInput.value = true;
|
||||
}
|
||||
if (!this.isActive.value) return;
|
||||
if (!isFunction(this.onkeypress)) return;
|
||||
this.onkeypress({
|
||||
key: ev.key,
|
||||
altKey: ev.altKey,
|
||||
ctrlKey: ev.ctrlKey,
|
||||
metaKey: ev.metaKey,
|
||||
shiftKey: ev.shiftKey,
|
||||
value,
|
||||
cancel() {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}
|
||||
});
|
||||
cursorHandler();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public blur(): void {
|
||||
if (this.el) this.el.blur();
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
if (this.el) this.el.focus();
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.isActive.value = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.isActive.value = true;
|
||||
}
|
||||
|
||||
public setValue(str: string): void {
|
||||
str = str || this.buffer;
|
||||
if (this.el) this.el.value = str;
|
||||
this.data.value = str;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.buffer = "";
|
||||
this.data.value = "";
|
||||
this.el.value = "";
|
||||
}
|
||||
|
||||
public pipe(output: IOutputInterface): void {
|
||||
const txtBefore = h<HTMLSpanElement>("span");
|
||||
const cursor = h<HTMLSpanElement>("span", {
|
||||
class: THEME.CURSOR,
|
||||
html: SPACE
|
||||
});
|
||||
const txtAfter = h<HTMLSpanElement>("span");
|
||||
|
||||
output.el?.append(txtBefore, cursor, txtAfter);
|
||||
|
||||
createEffect(() => {
|
||||
const i = this.ptr.value;
|
||||
const d = this.data.value;
|
||||
if (!this.isActive.value || !this.showInput.value) {
|
||||
txtBefore.textContent = "";
|
||||
cursor.innerHTML = SPACE;
|
||||
txtAfter.textContent = "";
|
||||
return;
|
||||
}
|
||||
txtBefore.textContent = d.substring(0, i);
|
||||
cursor.innerHTML = d.substring(i, i + 1).trim() || SPACE;
|
||||
txtAfter.textContent = d.substring(i + 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
47
xterminal/source/input/interface.ts
Normal file
47
xterminal/source/input/interface.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { IDisposable, IKeyPress } from "../types";
|
||||
import type { IOutputInterface } from "../output/interface";
|
||||
|
||||
/**
|
||||
* Interface: Input Component
|
||||
*/
|
||||
export interface IInputInterface extends IDisposable {
|
||||
/**
|
||||
* Blur the input element
|
||||
*/
|
||||
blur(): void;
|
||||
|
||||
/**
|
||||
* Focus the input element
|
||||
*/
|
||||
focus(): void;
|
||||
|
||||
/**
|
||||
* Deactivate the component
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Activate the component
|
||||
*/
|
||||
resume(): void;
|
||||
|
||||
/**
|
||||
* Callback function invoked on every key press
|
||||
*/
|
||||
onkeypress?: (ev: IKeyPress) => void;
|
||||
|
||||
/**
|
||||
* Bridge the input to the output component: cursor & input
|
||||
*/
|
||||
pipe(output: IOutputInterface): void;
|
||||
|
||||
/**
|
||||
* Set the value of the input element, updates the cursor
|
||||
*/
|
||||
setValue(str: string): void;
|
||||
|
||||
/**
|
||||
* Clears the value of the input element
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
169
xterminal/source/instance.ts
Normal file
169
xterminal/source/instance.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import XTerminal from "./index";
|
||||
import XHistory from "./history/index";
|
||||
import XOutputComponent from "./output/index";
|
||||
import XInputComponent from "./input/index";
|
||||
import { createEffect } from "./base/reactivity";
|
||||
import { NEWLINE, THEME, h, scrollDown } from "./renderer/dom";
|
||||
import { isFunction, isMobile } from "./helpers";
|
||||
import {
|
||||
ARROW_DOWN_KEY,
|
||||
ARROW_UP_KEY,
|
||||
ENTER_KEY,
|
||||
TAB_KEY,
|
||||
addEvent
|
||||
} from "./renderer/events";
|
||||
import { bounce } from "./base/debouncer";
|
||||
import { ITerminalState } from "./interface";
|
||||
|
||||
/**
|
||||
* Public Events
|
||||
*/
|
||||
export const DATA_EVENT = "data";
|
||||
export const CLEAR_EVENT = "clear";
|
||||
export const KEYPRESS_EVENT = "keypress";
|
||||
export const PAUSE_EVENT = "pause";
|
||||
export const RESUME_EVENT = "resume";
|
||||
|
||||
/**
|
||||
* Composes the components of the terminal object and integrates them
|
||||
* @param instance The terminal object
|
||||
* @param target The DOM element in which the terminal is to be mounted
|
||||
*/
|
||||
export function setup(
|
||||
instance: XTerminal,
|
||||
target: HTMLElement
|
||||
): ITerminalState {
|
||||
const term = h<HTMLDivElement>("div", {
|
||||
class: THEME.CONTAINER,
|
||||
props: { tabindex: 0 }
|
||||
});
|
||||
|
||||
const xhistory = new XHistory();
|
||||
const output = new XOutputComponent(term);
|
||||
const input = new XInputComponent(term);
|
||||
|
||||
const state = {
|
||||
input,
|
||||
output,
|
||||
history: xhistory,
|
||||
completer: (x: string) => x
|
||||
};
|
||||
|
||||
// connect the input to output (cursor & input text)
|
||||
input.pipe(output);
|
||||
|
||||
// scroll to the bottom on every output operation
|
||||
output.onoutput = () => scrollDown(term);
|
||||
|
||||
const checkScrollPosition = () => {
|
||||
if (input.isFocused.value) {
|
||||
// 给软键盘弹出和页面重排一点时间
|
||||
setTimeout(() => {
|
||||
let bottomInset = 0;
|
||||
if (window.visualViewport) {
|
||||
// 当键盘弹出时,如果布局视口高于可视视口,计算出被遮挡的高度(键盘+辅助栏等)
|
||||
bottomInset = Math.max(
|
||||
0,
|
||||
window.innerHeight -
|
||||
(window.visualViewport.height +
|
||||
window.visualViewport.offsetTop)
|
||||
);
|
||||
}
|
||||
|
||||
// 给输入区底部补充内间距,这样只需滚动内部容器,完全不需要让浏览器滚动 body (这会导致 Safari 底栏失去透明度)
|
||||
term.style.paddingBottom =
|
||||
bottomInset > 0
|
||||
? `calc(var(--xt-padding) + var(--tc-toolbar-height, 0px) + ${bottomInset}px)`
|
||||
: "";
|
||||
|
||||
scrollDown(term);
|
||||
|
||||
const cursor = term.querySelector(
|
||||
`.${THEME.CURSOR}`
|
||||
) as HTMLElement;
|
||||
if (cursor) {
|
||||
try {
|
||||
cursor.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest"
|
||||
});
|
||||
} catch {
|
||||
// fallback
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
} else {
|
||||
term.style.paddingBottom = "";
|
||||
}
|
||||
};
|
||||
|
||||
if (window.visualViewport) {
|
||||
instance.register(
|
||||
addEvent(window.visualViewport, "resize", checkScrollPosition)
|
||||
);
|
||||
} else {
|
||||
instance.register(addEvent(window, "resize", checkScrollPosition));
|
||||
}
|
||||
|
||||
instance.register(
|
||||
addEvent(term, "keydown", function (ev: KeyboardEvent) {
|
||||
// focus input element
|
||||
input.focus();
|
||||
// redirect the `keyboard` event to the input in the next event loop
|
||||
bounce(() => {
|
||||
input.el.dispatchEvent(new KeyboardEvent("keydown", ev));
|
||||
input.el.dispatchEvent(new KeyboardEvent("input", ev));
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
instance.register(
|
||||
addEvent(term, "focus", () => (input.isFocused.value = true))
|
||||
);
|
||||
|
||||
instance.register(
|
||||
addEvent(term, "blur", () => (input.isFocused.value = false))
|
||||
);
|
||||
|
||||
if (isMobile()) {
|
||||
// Toggle keyboard on mobile devices (touchscreen) on tap
|
||||
instance.register(addEvent(term, "click", input.focus.bind(input)));
|
||||
}
|
||||
|
||||
target.appendChild(term);
|
||||
|
||||
// handle keydown event
|
||||
input.onkeypress = (ev) => {
|
||||
if (ev.key == ENTER_KEY) {
|
||||
ev.cancel();
|
||||
xhistory.add(ev.value);
|
||||
output.writeSafe(ev.value + NEWLINE);
|
||||
instance.emit(DATA_EVENT, ev.value);
|
||||
} else if (ev.key == TAB_KEY) {
|
||||
ev.cancel();
|
||||
if (isFunction(state.completer)) {
|
||||
input.setValue(state.completer(ev.value));
|
||||
}
|
||||
} else if (ev.key == ARROW_DOWN_KEY) {
|
||||
ev.cancel();
|
||||
input.setValue(xhistory.next());
|
||||
} else if (ev.key == ARROW_UP_KEY) {
|
||||
ev.cancel();
|
||||
input.setValue(xhistory.previous());
|
||||
} else {
|
||||
instance.emit(KEYPRESS_EVENT, ev);
|
||||
}
|
||||
scrollDown(term);
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
// Fill the cursor if focused, otherwise outline
|
||||
if (input.isFocused.value) {
|
||||
term.classList.remove(THEME.INACTIVE);
|
||||
} else {
|
||||
term.classList.add(THEME.INACTIVE);
|
||||
}
|
||||
});
|
||||
|
||||
return state;
|
||||
}
|
||||
28
xterminal/source/interface.ts
Normal file
28
xterminal/source/interface.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { IInputInterface } from "./input/interface";
|
||||
import type { IOutputInterface } from "./output/interface";
|
||||
import type { IHistory } from "./history/interface";
|
||||
|
||||
/**
|
||||
* Terminal State
|
||||
*/
|
||||
export type ITerminalState = {
|
||||
/**
|
||||
* Input component for the terminal
|
||||
*/
|
||||
input: IInputInterface;
|
||||
|
||||
/**
|
||||
* Output component for the terminal
|
||||
*/
|
||||
output: IOutputInterface;
|
||||
|
||||
/**
|
||||
* History: store of inputs
|
||||
*/
|
||||
history: IHistory;
|
||||
|
||||
/**
|
||||
* Autocomplete function invoked on TAB key
|
||||
*/
|
||||
completer?: (data: string) => string;
|
||||
};
|
||||
65
xterminal/source/output/index.ts
Normal file
65
xterminal/source/output/index.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { isFunction } from "../helpers";
|
||||
import outputBuild from "../renderer/index";
|
||||
import { SPACE, h } from "../renderer/dom";
|
||||
import type { IOutputInterface } from "./interface";
|
||||
|
||||
const TAB_SIZE = 4;
|
||||
|
||||
function parseOutput(data = ""): string {
|
||||
return ("" + data)
|
||||
.replace(/(\n)|(\n\r)|(\r\n)/g, "<br/>")
|
||||
.replace(/\s{2}/g, SPACE.repeat(2))
|
||||
.replace(/\t/g, SPACE.repeat(TAB_SIZE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes user input so it can be safely rendered as HTML text.
|
||||
* - preserves all characters by converting them to HTML entities where needed.
|
||||
*/
|
||||
export function escapeHTML(data = ""): string {
|
||||
const span = document.createElement("span");
|
||||
span.textContent = data;
|
||||
return span.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Component
|
||||
*/
|
||||
export default class XOutputComponent implements IOutputInterface {
|
||||
public el: HTMLDivElement;
|
||||
private console: HTMLSpanElement;
|
||||
private lastOutput?: HTMLSpanElement;
|
||||
public onoutput?: () => void;
|
||||
|
||||
constructor(target: HTMLElement) {
|
||||
const { outputBox, consoleBox } = outputBuild(target);
|
||||
this.el = outputBox;
|
||||
this.console = consoleBox;
|
||||
}
|
||||
|
||||
public write(data: string, callback?: () => void): void {
|
||||
this.lastOutput = h<HTMLSpanElement>("span", {
|
||||
html: parseOutput(data)
|
||||
});
|
||||
this.console.appendChild(this.lastOutput);
|
||||
if (isFunction(this.onoutput)) this.onoutput();
|
||||
if (isFunction(callback)) callback();
|
||||
}
|
||||
|
||||
public writeSafe(data: string, callback?: () => void): void {
|
||||
this.write(escapeHTML(data), callback);
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (this.console) {
|
||||
this.console.innerHTML = "";
|
||||
}
|
||||
}
|
||||
|
||||
public clearLast(): void {
|
||||
if (this.lastOutput) {
|
||||
this.lastOutput.parentNode?.removeChild(this.lastOutput);
|
||||
}
|
||||
this.lastOutput = undefined;
|
||||
}
|
||||
}
|
||||
34
xterminal/source/output/interface.ts
Normal file
34
xterminal/source/output/interface.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Interface: Output Component
|
||||
*/
|
||||
export interface IOutputInterface {
|
||||
/**
|
||||
* Container element housing the console box
|
||||
*/
|
||||
el: HTMLDivElement;
|
||||
|
||||
/**
|
||||
* Inbuilt callback function for every write operation
|
||||
*/
|
||||
onoutput?: () => void;
|
||||
|
||||
/**
|
||||
* Output data to the console
|
||||
*/
|
||||
write(data: string, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Safely output data to the console
|
||||
*/
|
||||
writeSafe(data: string, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Clear the console
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Remove the element containing the previous output
|
||||
*/
|
||||
clearLast(): void;
|
||||
}
|
||||
5
xterminal/source/readme.md
Normal file
5
xterminal/source/readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# XTerminal
|
||||
|
||||
## Structure
|
||||
|
||||

|
||||
63
xterminal/source/renderer/dom.ts
Normal file
63
xterminal/source/renderer/dom.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { isArray, isObject } from "../helpers";
|
||||
import type { IElementProps } from "./interface";
|
||||
|
||||
export const NEWLINE = "\n";
|
||||
|
||||
export const SPACE = " ";
|
||||
|
||||
/**
|
||||
* CSS class names
|
||||
*/
|
||||
export const THEME = {
|
||||
CONTAINER: "xt",
|
||||
INACTIVE: "xt-inactive",
|
||||
CURSOR: "xt-cursor",
|
||||
OUTPUT: "xt-stdout",
|
||||
INPUT: "xt-stdin"
|
||||
};
|
||||
|
||||
export function scrollDown(el: HTMLElement): void {
|
||||
if (el) el.scrollTo(0, el.scrollHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ready to use HTMLElement
|
||||
*
|
||||
* https://github.com/henryhale/render-functions
|
||||
*
|
||||
* https://vuejs.org/guide/extras/render-function.html
|
||||
*
|
||||
* @param tag The HTML element tag
|
||||
* @param options The some properties of the element
|
||||
* @returns The HTML Element
|
||||
*/
|
||||
export function h<T extends HTMLElement>(
|
||||
tag: string,
|
||||
options?: IElementProps
|
||||
): T {
|
||||
const elem = document.createElement(tag);
|
||||
if (!isObject(options)) {
|
||||
return elem as T;
|
||||
}
|
||||
if (options?.id) {
|
||||
elem.id = options.id || "";
|
||||
}
|
||||
if (options?.class) {
|
||||
elem.className = options.class || "";
|
||||
}
|
||||
if (options?.content) {
|
||||
elem.appendChild(document.createTextNode(options.content || ""));
|
||||
}
|
||||
if (options?.html) {
|
||||
elem.innerHTML = options.html;
|
||||
}
|
||||
if (isArray(options?.children)) {
|
||||
options.children.forEach((c) => elem.append(c));
|
||||
}
|
||||
if (isObject(options?.props)) {
|
||||
Object.entries(options.props).forEach((v) =>
|
||||
elem.setAttribute(v[0], v[1])
|
||||
);
|
||||
}
|
||||
return elem as T;
|
||||
}
|
||||
28
xterminal/source/renderer/events.ts
Normal file
28
xterminal/source/renderer/events.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { IDisposable } from "../types";
|
||||
|
||||
export const ENTER_KEY = "Enter",
|
||||
TAB_KEY = "Tab",
|
||||
ARROW_UP_KEY = "ArrowUp",
|
||||
ARROW_DOWN_KEY = "ArrowDown";
|
||||
|
||||
/**
|
||||
* Attaches an event listener to the element returning a disposable object
|
||||
* to remove the event listener
|
||||
*/
|
||||
export function addEvent(
|
||||
el: Element | Document | Window | VisualViewport | EventTarget,
|
||||
type: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handler: (e: any) => void,
|
||||
opt?: boolean | AddEventListenerOptions
|
||||
): IDisposable {
|
||||
el.addEventListener(type, handler, opt);
|
||||
let disposed = false;
|
||||
return {
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
el.removeEventListener(type, handler, opt);
|
||||
disposed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
68
xterminal/source/renderer/index.ts
Normal file
68
xterminal/source/renderer/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { THEME, h } from "./dom";
|
||||
|
||||
/**
|
||||
* Generate a build for the output component
|
||||
* @param target The parent element in which it is mounted
|
||||
* @returns DOM reference to the console box and output container
|
||||
*/
|
||||
export default function outputBuild(target: HTMLElement) {
|
||||
const consoleBox = h<HTMLSpanElement>("span");
|
||||
|
||||
const outputBox = h<HTMLDivElement>("div", {
|
||||
class: THEME.OUTPUT,
|
||||
children: [consoleBox]
|
||||
});
|
||||
|
||||
target.appendChild(outputBox);
|
||||
|
||||
return { outputBox, consoleBox };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a build for the input component
|
||||
* @param target The parent element in which it is mounted
|
||||
* @returns DOM reference to the input element
|
||||
*/
|
||||
export function inputBuild(target: HTMLElement) {
|
||||
const inputBox = h<HTMLTextAreaElement>("textarea", {
|
||||
props: {
|
||||
// 明确声明为普通文本输入,降低移动端将其识别为登录表单的概率。
|
||||
spellcheck: false,
|
||||
autocorrect: "off",
|
||||
autocapitalize: "off",
|
||||
autocomplete: "off",
|
||||
name: "xterminal_input",
|
||||
inputmode: "text",
|
||||
enterkeyhint: "enter",
|
||||
rows: 1,
|
||||
wrap: "off"
|
||||
}
|
||||
});
|
||||
// 部分密码管理器会读取 data-* 做识别,显式标记为非登录用途。
|
||||
inputBox.setAttribute("data-form-type", "other");
|
||||
inputBox.setAttribute("data-lpignore", "true");
|
||||
inputBox.setAttribute("data-1p-ignore", "true");
|
||||
inputBox.setAttribute("data-bwignore", "true");
|
||||
inputBox.setAttribute("aria-autocomplete", "none");
|
||||
inputBox.setAttribute("autocapitalize", "off");
|
||||
inputBox.setAttribute("autocorrect", "off");
|
||||
// 某些浏览器在首帧点击文本输入框时会误触发凭据弹窗;
|
||||
// 先只读,获得焦点后立刻解除,可显著降低“刷新后点一下就弹登录”的概率。
|
||||
inputBox.readOnly = true;
|
||||
inputBox.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
inputBox.readOnly = false;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
const stdin = h<HTMLDivElement>("div", {
|
||||
class: THEME.INPUT,
|
||||
children: [inputBox]
|
||||
});
|
||||
|
||||
target.appendChild(stdin);
|
||||
|
||||
return inputBox;
|
||||
}
|
||||
37
xterminal/source/renderer/interface.ts
Normal file
37
xterminal/source/renderer/interface.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { IDisposable } from "../types";
|
||||
|
||||
/**
|
||||
* Render function - element props
|
||||
*/
|
||||
export interface IElementProps {
|
||||
id?: string;
|
||||
class?: string;
|
||||
content?: string;
|
||||
html?: string;
|
||||
children?: (string | Node)[];
|
||||
props?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Key Bindings to the Input
|
||||
*/
|
||||
interface IKeyBindingAction {
|
||||
(arg1: unknown, arg2?: unknown): void;
|
||||
}
|
||||
|
||||
export interface IKeyBindings {
|
||||
[key: string]: IKeyBindingAction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renderer
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
canInput: boolean;
|
||||
setKeyBindings(options: IKeyBindings): void;
|
||||
mount(el: HTMLElement): void;
|
||||
focusInput(): void;
|
||||
blurInput(): void;
|
||||
clearConsole(): void;
|
||||
output(data: string): void;
|
||||
}
|
||||
283
xterminal/source/types.d.ts
vendored
Normal file
283
xterminal/source/types.d.ts
vendored
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* @author Henry Hale <https://github.com/henryhale>
|
||||
* @license MIT
|
||||
*
|
||||
* This contains the type declarations for the `xterminal` library. Note that
|
||||
* some interfaces differ between this file and the actual implementation in
|
||||
* source/, that's because this file declares the *Public* API which is intended
|
||||
* to be stable and consumed by external programs.
|
||||
*/
|
||||
|
||||
/// <reference lib="dom"/>
|
||||
|
||||
/**
|
||||
* An object that can be disposed via a dispose function.
|
||||
*/
|
||||
export declare class IDisposable {
|
||||
/**
|
||||
* Clean up
|
||||
*/
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type of event name.
|
||||
*/
|
||||
export type IEventName = string | symbol;
|
||||
|
||||
/**
|
||||
* Callback function invoked when the event is dispatched.
|
||||
*/
|
||||
export type IEventListener = (...args: unknown[]) => void;
|
||||
|
||||
/**
|
||||
* Event map
|
||||
*/
|
||||
interface IEventMap {
|
||||
clear: () => void;
|
||||
data: (input: string) => void;
|
||||
keypress: (ev: IKeyPress) => void;
|
||||
pause: () => void;
|
||||
resume: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Object passed to callback functions invoked on `keypress` event
|
||||
*/
|
||||
export type IKeyPress = {
|
||||
key: string;
|
||||
value: string;
|
||||
altKey: boolean;
|
||||
metaKey: boolean;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
cancel(): void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Event emitter
|
||||
*
|
||||
* It extends the Disposable class.
|
||||
*/
|
||||
export declare class XEventEmitter extends IDisposable {
|
||||
/**
|
||||
* Appends a event listener to the specified event.
|
||||
*
|
||||
* The listener is invoked everytime the event is dispatched.
|
||||
*
|
||||
* - return disposable object to remove the event listener
|
||||
*/
|
||||
on<K extends keyof IEventMap>(event: K, listener: IEventMap[K]): void;
|
||||
on(event: IEventName, listener: IEventListener): void;
|
||||
|
||||
/**
|
||||
* Appends a event listener to the specified event.
|
||||
*
|
||||
* The listener is invoked _only once_ when the event is dispatched.
|
||||
*
|
||||
* _It is deleted thereafter._
|
||||
*/
|
||||
once<K extends keyof IEventMap>(event: K, listener: IEventMap[K]): void;
|
||||
once(event: IEventName, listener: IEventListener): void;
|
||||
|
||||
/**
|
||||
* Removes an event listener from the specified event.
|
||||
*
|
||||
* The listener won't be invoked on event dispatch thereafter.
|
||||
*/
|
||||
off<K extends keyof IEventMap>(event: K, listener: IEventMap[K]): void;
|
||||
off(event: IEventName, listener: IEventListener): void;
|
||||
|
||||
/**
|
||||
* Triggers an event
|
||||
* @param event The event name to dispatch.
|
||||
* @param args data to be passed to the event listener.
|
||||
*/
|
||||
emit<K extends keyof IEventMap>(event: K, ...args: unknown[]): void;
|
||||
emit(event: IEventName, ...args: unknown[]): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminal Options
|
||||
*/
|
||||
export type ITerminalOptions = {
|
||||
/**
|
||||
* An HTMLElement in which the terminal will be mounted
|
||||
*/
|
||||
target?: HTMLElement;
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin
|
||||
*/
|
||||
// export interface IPlugin {
|
||||
// install(context: IPluginContext): void;
|
||||
// }
|
||||
|
||||
/**
|
||||
* XTerminal
|
||||
*
|
||||
* Create a new terminal instance
|
||||
*/
|
||||
declare class XTerminal extends XEventEmitter {
|
||||
constructor(options?: ITerminalOptions);
|
||||
|
||||
/**
|
||||
* Mounts the terminal instance in the `target` HTMLElement.
|
||||
*
|
||||
* If the selector is given, the first element is used.
|
||||
*
|
||||
* @param target An HTMLElement in which the terminal will be mounted.
|
||||
*/
|
||||
mount(target: HTMLElement | string): void;
|
||||
|
||||
/**
|
||||
* Focus the terminal - ready for input.
|
||||
*/
|
||||
focus(): void;
|
||||
|
||||
/**
|
||||
* Blurs the terminal.
|
||||
*/
|
||||
blur(): void;
|
||||
|
||||
/**
|
||||
* Write data to the terminal.
|
||||
*
|
||||
* - use `XTerminal.writeSafe` when printing arbitrary data like user input
|
||||
*
|
||||
* @param data The data to write to the terminal
|
||||
* @param callback Optional function invoked on successful write
|
||||
* @returns void
|
||||
*/
|
||||
write(data: string | number, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Safely write data to the terminal.
|
||||
*
|
||||
* (recommended) to prevent malicious attacks like XSS
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* term.writeSafe('<h1>hello</h1>');
|
||||
* // <h1>hello</h1>
|
||||
* ```
|
||||
*
|
||||
* @param data The data to write to the terminal
|
||||
* @param callback Optional function invoked on successful write
|
||||
* @returns void
|
||||
*/
|
||||
writeSafe(data: string | number, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Write data to the terminal, followed by a break line character (\n).
|
||||
*
|
||||
* - use `XTerminal.writelnSafe` when printing arbitrary data like user input
|
||||
*
|
||||
* @param data The data to write to the terminal
|
||||
* @param callback Optional function invoked on successful write
|
||||
* @returns void
|
||||
*/
|
||||
writeln(data: string | number, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Safely write data to the terminal, followed by a break line character (\n).
|
||||
*
|
||||
* (recommended) to prevent malicious attacks like XSS
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* term.writelnSafe('<h1>hello</h1>');
|
||||
* // <h1>hello</h1><br/>
|
||||
* ```
|
||||
* @param data The data to write to the terminal
|
||||
* @param callback Optional function invoked on successful write
|
||||
* @returns void
|
||||
*/
|
||||
writelnSafe(data: string | number, callback?: () => void): void;
|
||||
|
||||
/**
|
||||
* Clear the entire terminal.
|
||||
*
|
||||
* This method triggers the `clear` event.
|
||||
*/
|
||||
clear(): void;
|
||||
|
||||
/**
|
||||
* Remove the element containing the previous output
|
||||
*/
|
||||
clearLast(): void;
|
||||
|
||||
/**
|
||||
* Access the history stack
|
||||
*/
|
||||
history: string[];
|
||||
|
||||
/**
|
||||
* Clears the entire history stack.
|
||||
*/
|
||||
clearHistory(): void;
|
||||
|
||||
/**
|
||||
* Sets the autocomplete function that is invoked on Tab key.
|
||||
*
|
||||
* @param fn completer function that takes a string input and return a better
|
||||
* completion.
|
||||
*/
|
||||
setCompleter(fn: (data: string) => string): void;
|
||||
|
||||
/**
|
||||
* Deactivate the terminal input
|
||||
*/
|
||||
pause(): void;
|
||||
|
||||
/**
|
||||
* Activate the terminal input
|
||||
*/
|
||||
resume(): void;
|
||||
|
||||
/**
|
||||
* Gracefully close the terminal instance.
|
||||
*
|
||||
* This detaches all event listeners, unmounts the terminal from the DOM,
|
||||
* and clears the backing functionality of the terminal.
|
||||
*
|
||||
* _The terminal should not be used again once disposed._
|
||||
*
|
||||
*/
|
||||
dispose(): void;
|
||||
|
||||
/**
|
||||
* Install an addon/plugin
|
||||
*/
|
||||
// use(plugin: IPlugin): void;
|
||||
|
||||
/**
|
||||
* Version number
|
||||
*/
|
||||
static readonly version: string;
|
||||
|
||||
/**
|
||||
* Event emitter
|
||||
*/
|
||||
static readonly XEventEmitter: XEventEmitter;
|
||||
|
||||
/**
|
||||
* Escapes user input so it can be safely rendered as HTML text.
|
||||
* - preserves all characters by converting them to HTML entities where needed.
|
||||
* - recommended for use on user input or any arbitrary data
|
||||
*/
|
||||
static escapeHTML(data?: string): string;
|
||||
|
||||
/**
|
||||
* Clears the input buffer
|
||||
*/
|
||||
clearInput(): void;
|
||||
|
||||
/**
|
||||
* Sets the input buffer to some value, updates the cursor
|
||||
*/
|
||||
setInput(str: string): void;
|
||||
}
|
||||
|
||||
export default XTerminal;
|
||||
Reference in New Issue
Block a user