@jupyterlite/terminal
Version:
A terminal for JupyterLite
182 lines (156 loc) • 5.21 kB
text/typescript
import { PageConfig, URLExt } from '@jupyterlab/coreutils';
import { ServerConnection, Terminal } from '@jupyterlab/services';
import {
IExternalCommand,
IShellManager,
IStdinReply,
IStdinRequest,
ShellManager
} from '@jupyterlite/cockle';
import { JSONPrimitive } from '@lumino/coreutils';
import {
Server as WebSocketServer,
Client as WebSocketClient
} from 'mock-socket';
import { Shell } from './shell';
import { ILiteTerminalAPIClient } from './tokens';
export class LiteTerminalAPIClient implements ILiteTerminalAPIClient {
constructor(options: { serverSettings?: ServerConnection.ISettings } = {}) {
this.serverSettings =
options.serverSettings ?? ServerConnection.makeSettings();
this._shellManager = new ShellManager();
}
/**
* Set identifier for communicating with service worker.
*/
set browsingContextId(browsingContextId: string) {
console.log('LiteTerminalAPIClient browsingContextId', browsingContextId);
this._browsingContextId = browsingContextId;
}
/**
* Function that handles stdin requests received from service worker.
*/
async handleStdin(request: IStdinRequest): Promise<IStdinReply> {
return await this._shellManager.handleStdin(request);
}
get isAvailable(): boolean {
const available = String(PageConfig.getOption('terminalsAvailable'));
return available.toLowerCase() === 'true';
}
readonly serverSettings: ServerConnection.ISettings;
async startNew(
options?: Terminal.ITerminal.IOptions
): Promise<Terminal.IModel> {
// Create shell.
const name = options?.name ?? 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 => {
const msg = JSON.stringify(['stdout', text]);
shell.socket?.send(msg);
}
});
this._shells.set(name, shell);
// Hook to connect socket to shell.
const hook = async (
shell: Shell,
socket: WebSocketClient
): Promise<void> => {
shell.socket = socket;
socket.on('message', async (message: any) => {
// Message from xtermjs to pass to shell.
const data = JSON.parse(message) as JSONPrimitive[];
const message_type = data[0];
const content = data.slice(1);
await shell.ready;
if (message_type === 'stdin') {
await shell.input(content[0] as string);
} else if (message_type === 'set_size') {
const rows = content[0] as number;
const columns = content[1] as number;
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: WebSocketClient): void => {
hook(shell, socket);
});
shell.disposed.connect(() => {
this.shutdown(name);
wsServer.close();
});
return { name };
}
async listRunning(): Promise<Terminal.IModel[]> {
return this._models;
}
registerAlias(key: string, value: string): void {
if (this._aliases === undefined) {
this._aliases = {};
}
this._aliases[key] = value;
}
registerEnvironmentVariable(key: string, value: string | undefined): void {
if (this._environment === undefined) {
this._environment = {};
}
this._environment[key] = value;
}
registerExternalCommand(options: IExternalCommand.IOptions): void {
this._externalCommands.push(options);
}
async shutdown(name: string): Promise<void> {
const shell = this._shells.get(name);
if (shell !== undefined) {
shell.socket?.send(JSON.stringify(['disconnect']));
shell.socket?.close();
this._shells.delete(name);
shell.dispose();
}
}
themeChange(isDarkMode?: boolean): void {
for (const shell of this._shells.values()) {
// Can pass isDarkMode when cockle is released with PR #232.
//shell.themeChange(isDarkMode);
shell.themeChange();
}
}
private get _models(): Terminal.IModel[] {
return Array.from(this._shells.keys(), name => {
return { name };
});
}
private _nextAvailableName(): string {
for (let i = 1; ; ++i) {
const name = `${i}`;
if (!this._shells.has(name)) {
return name;
}
}
}
private _aliases?: { [key: string]: string };
private _environment?: { [key: string]: string | undefined };
private _browsingContextId?: string;
private _externalCommands: IExternalCommand.IOptions[] = [];
private _shellManager: IShellManager;
private _shells = new Map<string, Shell>();
}