@jupyterlite/terminal
Version:
A terminal for JupyterLite
142 lines (141 loc) • 5.17 kB
JavaScript
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { ServerConnection } from '@jupyterlab/services';
import { ShellManager } from '@jupyterlite/cockle';
import { Signal } from '@lumino/signaling';
import { Server as WebSocketServer } from 'mock-socket';
import { Shell } from './shell';
export class LiteTerminalAPIClient {
constructor(options = {}) {
var _a;
this._externalCommands = [];
this._shells = new Map();
this._terminalDisposed = new Signal(this);
this.serverSettings =
(_a = options.serverSettings) !== null && _a !== void 0 ? _a : ServerConnection.makeSettings();
this._shellManager = new ShellManager();
}
/**
* Set identifier for communicating with service worker.
*/
set browsingContextId(browsingContextId) {
console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
this._browsingContextId = browsingContextId;
}
/**
* Function that handles stdin requests received from service worker.
*/
async handleStdin(request) {
return await this._shellManager.handleStdin(request);
}
get isAvailable() {
const available = String(PageConfig.getOption('terminalsAvailable'));
return available.toLowerCase() === 'true';
}
async startNew(options) {
var _a;
// Create shell.
const name = (_a = options === null || options === void 0 ? void 0 : options.name) !== null && _a !== void 0 ? _a : this._nextAvailableName();
const { baseUrl, wsUrl } = this.serverSettings;
const shell = new Shell({
mountpoint: '/drive',
baseUrl,
wasmBaseUrl: URLExt.join(baseUrl, 'extensions/@jupyterlite/terminal/static/wasm/'),
browsingContextId: this._browsingContextId,
aliases: this._aliases,
environment: this._environment,
externalCommands: this._externalCommands,
shellId: name,
shellManager: this._shellManager,
outputCallback: text => {
var _a;
const msg = JSON.stringify(['stdout', text]);
(_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(msg);
}
});
this._shells.set(name, shell);
// Hook to connect socket to shell.
const hook = async (shell, socket) => {
shell.socket = socket;
socket.on('message', async (message) => {
// Message from xtermjs to pass to shell.
const data = JSON.parse(message);
const message_type = data[0];
const content = data.slice(1);
await shell.ready;
if (message_type === 'stdin') {
await shell.input(content[0]);
}
else if (message_type === 'set_size') {
const rows = content[0];
const columns = content[1];
await shell.setSize(rows, columns);
}
});
// Return handshake.
const res = JSON.stringify(['setup']);
console.log('Terminal returning handshake via socket');
socket.send(res);
shell.start();
};
const url = URLExt.join(wsUrl, 'terminals', 'websocket', name);
const wsServer = new WebSocketServer(url);
wsServer.on('connection', (socket) => {
hook(shell, socket);
});
shell.disposed.connect(() => {
this.shutdown(name);
wsServer.close();
this._terminalDisposed.emit(shell.shellId);
});
return { name };
}
async listRunning() {
return this._models;
}
registerAlias(key, value) {
if (this._aliases === undefined) {
this._aliases = {};
}
this._aliases[key] = value;
}
registerEnvironmentVariable(key, value) {
if (this._environment === undefined) {
this._environment = {};
}
this._environment[key] = value;
}
registerExternalCommand(options) {
this._externalCommands.push(options);
}
async shutdown(name) {
var _a, _b;
const shell = this._shells.get(name);
if (shell !== undefined) {
(_a = shell.socket) === null || _a === void 0 ? void 0 : _a.send(JSON.stringify(['disconnect']));
(_b = shell.socket) === null || _b === void 0 ? void 0 : _b.close();
this._shells.delete(name);
shell.dispose();
}
}
get terminalDisposed() {
return this._terminalDisposed;
}
themeChange(isDarkMode) {
for (const shell of this._shells.values()) {
shell.themeChange(isDarkMode);
}
}
get _models() {
return Array.from(this._shells.keys(), name => {
return { name };
});
}
_nextAvailableName() {
for (let i = 1;; ++i) {
const name = `${i}`;
if (!this._shells.has(name)) {
return name;
}
}
}
}