mirror of
https://github.com/pyscript/pyscript.git
synced 2026-03-24 11:00:38 -04:00
Add Option to make Py-Terminal and Xterm.js (#1317)
* Add 'xterm' attribute in py-config using new validation * Use screen reader mode * Add `xtermReady` promise to allow users to away xterm.js init * Guard against initializing a tag twice * Add tests and doc
This commit is contained in:
@@ -6,14 +6,18 @@ import { Plugin, validateConfigParameterFromArray } from '../plugin';
|
||||
import { getLogger } from '../logger';
|
||||
import { type Stdio } from '../stdio';
|
||||
import { InterpreterClient } from '../interpreter_client';
|
||||
import { Terminal as TerminalType } from 'xterm';
|
||||
|
||||
const logger = getLogger('py-terminal');
|
||||
const knownPyTerminalTags: WeakSet<HTMLElement> = new WeakSet();
|
||||
|
||||
type AppConfigStyle = AppConfig & {
|
||||
terminal?: string | boolean;
|
||||
docked?: string | boolean;
|
||||
terminal?: boolean | 'auto';
|
||||
docked?: boolean | 'docked';
|
||||
xterm?: boolean | 'xterm';
|
||||
};
|
||||
|
||||
const logger = getLogger('py-terminal');
|
||||
|
||||
export class PyTerminalPlugin extends Plugin {
|
||||
app: PyScriptApp;
|
||||
|
||||
@@ -36,19 +40,27 @@ export class PyTerminalPlugin extends Plugin {
|
||||
possibleValues: [true, false, 'docked'],
|
||||
defaultValue: 'docked',
|
||||
});
|
||||
validateConfigParameterFromArray({
|
||||
config: config,
|
||||
name: 'xterm',
|
||||
possibleValues: [true, false, 'xterm'],
|
||||
defaultValue: false,
|
||||
});
|
||||
}
|
||||
|
||||
beforeLaunch(config: AppConfigStyle) {
|
||||
// if config.terminal is "yes" or "auto", let's add a <py-terminal> to
|
||||
// the document, unless it's already present.
|
||||
const { terminal: t, docked: d } = config;
|
||||
const { terminal: t, docked: d, xterm: x } = config;
|
||||
const auto = t === true || t === 'auto';
|
||||
const docked = d === true || d === 'docked';
|
||||
const xterm = x === true || x === 'xterm';
|
||||
if (auto && $('py-terminal', document) === null) {
|
||||
logger.info('No <py-terminal> found, adding one');
|
||||
const termElem = document.createElement('py-terminal');
|
||||
if (auto) termElem.setAttribute('auto', '');
|
||||
if (docked) termElem.setAttribute('docked', '');
|
||||
if (xterm) termElem.setAttribute('xterm', '');
|
||||
document.body.appendChild(termElem);
|
||||
}
|
||||
}
|
||||
@@ -57,7 +69,8 @@ export class PyTerminalPlugin extends Plugin {
|
||||
// the Python interpreter has been initialized and we are ready to
|
||||
// execute user code:
|
||||
//
|
||||
// 1. define the "py-terminal" custom element
|
||||
// 1. define the "py-terminal" custom element, either a <pre> element
|
||||
// or using xterm.js
|
||||
//
|
||||
// 2. if there is a <py-terminal> tag on the page, it will register
|
||||
// a Stdio listener just before the user code executes, ensuring
|
||||
@@ -70,48 +83,58 @@ export class PyTerminalPlugin extends Plugin {
|
||||
//
|
||||
// 4. (in the future we might want to add an option to start the
|
||||
// capture earlier, but I don't think it's important now).
|
||||
const PyTerminal = make_PyTerminal(this.app);
|
||||
const PyTerminal = _interpreter.config.xterm ? make_PyTerminal_xterm(this.app) : make_PyTerminal_pre(this.app);
|
||||
customElements.define('py-terminal', PyTerminal);
|
||||
}
|
||||
}
|
||||
|
||||
function make_PyTerminal(app: PyScriptApp) {
|
||||
abstract class PyTerminalBaseClass extends HTMLElement implements Stdio {
|
||||
autoShowOnNextLine: boolean;
|
||||
|
||||
isAuto() {
|
||||
return this.hasAttribute('auto');
|
||||
}
|
||||
|
||||
isDocked() {
|
||||
return this.hasAttribute('docked');
|
||||
}
|
||||
|
||||
setupPosition(app: PyScriptApp) {
|
||||
if (this.isAuto()) {
|
||||
this.classList.add('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = true;
|
||||
} else {
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
|
||||
if (this.isDocked()) {
|
||||
this.classList.add('py-terminal-docked');
|
||||
}
|
||||
|
||||
logger.info('Registering stdio listener');
|
||||
app.registerStdioListener(this);
|
||||
}
|
||||
|
||||
abstract stdout_writeline(msg: string): void;
|
||||
abstract stderr_writeline(msg: string): void;
|
||||
}
|
||||
|
||||
function make_PyTerminal_pre(app: PyScriptApp) {
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminal extends HTMLElement implements Stdio {
|
||||
class PyTerminalPre extends PyTerminalBaseClass {
|
||||
outElem: HTMLElement;
|
||||
autoShowOnNextLine: boolean;
|
||||
|
||||
connectedCallback() {
|
||||
// should we use a shadowRoot instead? It looks unnecessarily
|
||||
// complicated to me, but I'm not really sure about the
|
||||
// implications
|
||||
this.outElem = document.createElement('pre');
|
||||
this.outElem.className = 'py-terminal';
|
||||
this.outElem.classList.add('py-terminal');
|
||||
this.appendChild(this.outElem);
|
||||
|
||||
if (this.isAuto()) {
|
||||
this.classList.add('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = true;
|
||||
} else {
|
||||
this.autoShowOnNextLine = false;
|
||||
}
|
||||
|
||||
if (this.isDocked()) {
|
||||
this.classList.add('py-terminal-docked');
|
||||
}
|
||||
|
||||
logger.info('Registering stdio listener');
|
||||
app.registerStdioListener(this);
|
||||
}
|
||||
|
||||
isAuto() {
|
||||
return this.hasAttribute('auto');
|
||||
}
|
||||
|
||||
isDocked() {
|
||||
return this.hasAttribute('docked');
|
||||
this.setupPosition(app);
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
@@ -132,5 +155,121 @@ function make_PyTerminal(app: PyScriptApp) {
|
||||
// end of the Stdio interface
|
||||
}
|
||||
|
||||
return PyTerminal;
|
||||
return PyTerminalPre;
|
||||
}
|
||||
|
||||
declare const Terminal: typeof TerminalType;
|
||||
|
||||
function make_PyTerminal_xterm(app: PyScriptApp) {
|
||||
/** The <py-terminal> custom element, which automatically register a stdio
|
||||
* listener to capture and display stdout/stderr
|
||||
*/
|
||||
class PyTerminalXterm extends PyTerminalBaseClass {
|
||||
outElem: HTMLDivElement;
|
||||
_moduleResolved: boolean;
|
||||
xtermReady: Promise<TerminalType>;
|
||||
xterm: TerminalType;
|
||||
cachedStdOut: Array<string>;
|
||||
cachedStdErr: Array<string>;
|
||||
_xterm_cdn_base_url = 'https://cdn.jsdelivr.net/npm/xterm@5.1.0';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.cachedStdOut = [];
|
||||
this.cachedStdErr = [];
|
||||
|
||||
// While this is false, store writes to stdout/stderr to a buffer
|
||||
// when the xterm.js is actually ready, we will "replay" those writes
|
||||
// and set this to true
|
||||
this._moduleResolved = false;
|
||||
|
||||
//Required to make xterm appear properly
|
||||
this.style.width = '100%';
|
||||
this.style.height = '100%';
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
//guard against initializing a tag twice
|
||||
if (knownPyTerminalTags.has(this)) return;
|
||||
knownPyTerminalTags.add(this);
|
||||
|
||||
this.outElem = document.createElement('div');
|
||||
//this.outElem.className = 'py-terminal';
|
||||
this.appendChild(this.outElem);
|
||||
|
||||
this.setupPosition(app);
|
||||
|
||||
this.xtermReady = this._setupXterm();
|
||||
await this.xtermReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the xtermjs library from CDN an initialize it.
|
||||
* @private
|
||||
* @returns the associated xterm.js Terminal
|
||||
*/
|
||||
async _setupXterm() {
|
||||
if (this.xterm == undefined) {
|
||||
//need to initialize the Terminal for this element
|
||||
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
if (globalThis.Terminal == undefined) {
|
||||
//load xterm module from cdn
|
||||
//eslint-disable-next-line
|
||||
//@ts-ignore
|
||||
await import(this._xterm_cdn_base_url + '/lib/xterm.js');
|
||||
|
||||
const cssTag = document.createElement('link');
|
||||
cssTag.type = 'text/css';
|
||||
cssTag.rel = 'stylesheet';
|
||||
cssTag.href = this._xterm_cdn_base_url + '/css/xterm.css';
|
||||
document.head.appendChild(cssTag);
|
||||
}
|
||||
|
||||
//Create xterm, add addons
|
||||
this.xterm = new Terminal({ screenReaderMode: true, cols: 80 });
|
||||
|
||||
// xterm must only 'open' into a visible DOM element
|
||||
// If terminal is still hidden, open during first write
|
||||
if (!this.autoShowOnNextLine) this.xterm.open(this);
|
||||
|
||||
this._moduleResolved = true;
|
||||
|
||||
//Write out any messages output while xterm was loading
|
||||
this.cachedStdOut.forEach((value: string): void => this.stdout_writeline(value));
|
||||
this.cachedStdErr.forEach((value: string): void => this.stderr_writeline(value));
|
||||
} else {
|
||||
this._moduleResolved = true;
|
||||
}
|
||||
return this.xterm;
|
||||
}
|
||||
|
||||
// implementation of the Stdio interface
|
||||
stdout_writeline(msg: string) {
|
||||
if (this._moduleResolved) {
|
||||
this.xterm.writeln(msg);
|
||||
//this.outElem.innerText += msg + '\n';
|
||||
|
||||
if (this.isDocked()) {
|
||||
this.scrollTop = this.scrollHeight;
|
||||
}
|
||||
if (this.autoShowOnNextLine) {
|
||||
this.classList.remove('py-terminal-hidden');
|
||||
this.autoShowOnNextLine = false;
|
||||
this.xterm.open(this);
|
||||
}
|
||||
} else {
|
||||
//if xtermjs not loaded, cache messages
|
||||
this.cachedStdOut.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
stderr_writeline(msg: string) {
|
||||
this.stdout_writeline(msg);
|
||||
}
|
||||
// end of the Stdio interface
|
||||
}
|
||||
|
||||
return PyTerminalXterm;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user