diff --git a/src/app/plugins/notebook/commands.ts b/src/app/plugins/notebook/commands.ts
index 9f2c7ce..41daa84 100644
--- a/src/app/plugins/notebook/commands.ts
+++ b/src/app/plugins/notebook/commands.ts
@@ -96,10 +96,18 @@ export const addCommands = (
});
commands.addCommand(CommandIDs.new, {
- label: 'New Notebook',
- execute: async () => {
+ label: (args: any) => {
+ const kernelName = args['kernelName'];
+ if (args['isLauncher'] && kernelName) {
+ return kernelName;
+ }
+ return 'New Notebook';
+ },
+ execute: (args: any) => {
+ const kernelName = args['kernelName'] as string;
commands.execute(CommandIDs.open, {
- name: 'untitled.ipynb'
+ name: 'untitled.ipynb',
+ kernelName
});
}
});
@@ -108,13 +116,16 @@ export const addCommands = (
label: (args: any) => {
const name = args['name'] as string;
if (name) {
- return `Open ${name}`;
+ return `${name}`;
}
return 'Open Example';
},
execute: async args => {
const name = args['name'] as string;
- const notebook = docManager.open(name) as NotebookPanel;
+ const kernelName = args['kernelName'] as string;
+ const notebook = docManager.open(name, 'default', {
+ name: kernelName
+ }) as NotebookPanel;
const sessionContext = notebook.sessionContext;
await sessionContext.ready;
diff --git a/src/resources/pyp5js.svg b/src/resources/pyp5js.svg
new file mode 100644
index 0000000..752fc82
--- /dev/null
+++ b/src/resources/pyp5js.svg
@@ -0,0 +1,93 @@
+
+
diff --git a/src/server/contents.ts b/src/server/contents.ts
index 4fcc017..5067fb8 100644
--- a/src/server/contents.ts
+++ b/src/server/contents.ts
@@ -116,23 +116,7 @@ namespace Private {
*/
const EMPTY_NB: INotebookContent = {
metadata: {
- orig_nbformat: 4,
- kernelspec: {
- name: 'p5.js',
- display_name: 'p5.js'
- },
- language_info: {
- codemirror_mode: {
- name: 'javascript',
- version: 3
- },
- file_extension: '.js',
- mimetype: 'text/javascript',
- name: 'javascript',
- nbconvert_exporter: 'javascript',
- pygments_lexer: 'javascript',
- version: 'es2017'
- }
+ orig_nbformat: 4
},
nbformat_minor: 4,
nbformat: 4,
@@ -149,7 +133,7 @@ namespace Private {
created: '2020-03-18T18:41:01.243007Z',
content: EMPTY_NB,
format: 'json',
- mimetype: '',
+ mimetype: 'application/json',
size: JSON.stringify(EMPTY_NB).length,
writable: true,
type: 'notebook'
diff --git a/src/server/kernels.ts b/src/server/kernels.ts
index 66f7e24..dc09e2e 100644
--- a/src/server/kernels.ts
+++ b/src/server/kernels.ts
@@ -9,9 +9,10 @@ import {
import { Server as WebSocketServer } from 'mock-socket';
-import { KernelIFrame } from './kernel';
+import { KernelIFrame } from './kernels/kernel';
import { IJupyterServer } from '../tokens';
+import { PyP5KernelIFrame } from './kernels/pyp5Kernel';
/**
* A class to handle requests to /api/kernels
@@ -20,10 +21,10 @@ export class Kernels implements IJupyterServer.IRoutable {
/**
* Start a new kernel.
*
- * @param sessionId The session id.
+ * @param options The kernel start options.
*/
- startNew(sessionId: string): Kernel.IModel {
- const id = sessionId;
+ startNew(options: Kernels.IStartOptions): Kernel.IModel {
+ const { id, name } = options;
const kernelUrl = `${Kernels.WS_BASE_URL}/api/kernels/${id}/channels`;
const wsServer = new WebSocketServer(kernelUrl);
@@ -34,7 +35,14 @@ export class Kernels implements IJupyterServer.IRoutable {
socket.send(message);
};
- const kernel = new KernelIFrame({ id, sendMessage, sessionId });
+ let kernel: IJupyterServer.IKernelIFrame;
+
+ // TODO: more generic kernel instantiation
+ if (name === 'pyp5') {
+ kernel = new PyP5KernelIFrame({ id, sendMessage, sessionId: id });
+ } else {
+ kernel = new KernelIFrame({ id, sendMessage, sessionId: id });
+ }
this._kernels.set(id, kernel);
socket.on('message', (message: string | ArrayBuffer) => {
@@ -53,7 +61,7 @@ export class Kernels implements IJupyterServer.IRoutable {
const model = {
id,
- name: 'p5.js'
+ name: name ?? 'p5.js'
};
return model;
}
@@ -81,13 +89,28 @@ export class Kernels implements IJupyterServer.IRoutable {
return new Response(null);
}
- private _kernels = new ObservableMap();
+ private _kernels = new ObservableMap();
}
/**
* A namespace for Kernels statics.
*/
export namespace Kernels {
+ /**
+ * Options to start a new options.
+ */
+ export interface IStartOptions {
+ /**
+ * The kernel id.
+ */
+ id: string;
+
+ /**
+ * The kernel name.
+ */
+ name?: string;
+ }
+
/**
* The base url for the Kernels manager
*/
diff --git a/src/server/kernel.ts b/src/server/kernels/kernel.ts
similarity index 99%
rename from src/server/kernel.ts
rename to src/server/kernels/kernel.ts
index 984c897..864f4c5 100644
--- a/src/server/kernel.ts
+++ b/src/server/kernels/kernel.ts
@@ -8,7 +8,7 @@ import p5 from '!!raw-loader!p5/lib/p5.min.js';
import p5Sound from '!!raw-loader!p5/lib/addons/p5.sound.min.js';
-import { IJupyterServer } from '../tokens';
+import { IJupyterServer } from '../../tokens';
/**
* A kernel that executes code in an IFrame.
diff --git a/src/server/kernels/pyp5Kernel.ts b/src/server/kernels/pyp5Kernel.ts
new file mode 100644
index 0000000..09239fe
--- /dev/null
+++ b/src/server/kernels/pyp5Kernel.ts
@@ -0,0 +1,542 @@
+import { KernelMessage } from '@jupyterlab/services';
+
+import { PromiseDelegate } from '@lumino/coreutils';
+
+import { IDisposable } from '@lumino/disposable';
+
+import { IJupyterServer } from '../../tokens';
+
+/**
+ * A kernel that executes code in an IFrame.
+ */
+export class PyP5KernelIFrame
+ implements IJupyterServer.IKernelIFrame, IDisposable {
+ /**
+ * Instantiate a new IFrameKernel
+ *
+ * @param options The instantiation options for a new IFrameKernel
+ */
+ constructor(options: IFrameKernel.IOptions) {
+ const { id, sendMessage, sessionId } = options;
+ this._id = id;
+ // TODO: handle session id
+ this._sessionId = sessionId;
+ this._sendMessage = sendMessage;
+
+ // create the main IFrame
+ this._iframe = document.createElement('iframe');
+ this._iframe.style.visibility = 'hidden';
+ document.body.appendChild(this._iframe);
+
+ this._initIFrame(this._iframe).then(() => {
+ // TODO: handle kernel ready
+ this._jsFunc(
+ this._iframe.contentWindow,
+ `
+ console._log = console.log;
+ console._error = console.error;
+
+ window._bubbleUp = function(msg) {
+ window.parent.postMessage({
+ ...msg,
+ "parentHeader": window._parentHeader
+ });
+ }
+
+ console.log = function() {
+ const args = Array.prototype.slice.call(arguments);
+ window._bubbleUp({
+ "event": "stream",
+ "name": "stdout",
+ "text": args.join(' ') + '\\n'
+ });
+ };
+ console.info = console.log;
+
+ console.error = function() {
+ const args = Array.prototype.slice.call(arguments);
+ window._bubbleUp({
+ "event": "stream",
+ "name": "stderr",
+ "text": args.join(' ') + '\\n'
+ });
+ };
+ console.warn = console.error;
+
+ window.onerror = function(message, source, lineno, colno, error) {
+ console.error(message);
+ }
+
+ setTimeout(() => {
+ languagePluginLoader.then(() => {
+ console.log('pyodide loaded!');
+ const res = window.pyodide.runPython("1 + 2");
+ console.log(res);
+ });
+ }, 1000);
+ `
+ );
+ window.addEventListener('message', (e: MessageEvent) => {
+ const msg = e.data;
+ const parentHeader = msg.parentHeader as KernelMessage.IHeader<
+ KernelMessage.MessageType
+ >;
+ if (msg.event === 'stream') {
+ const content = msg as KernelMessage.IStreamMsg['content'];
+ this._stream(parentHeader, content);
+ }
+ });
+ });
+ }
+
+ /**
+ * Whether the kernel is disposed.
+ */
+ get isDisposed(): boolean {
+ return this._isDisposed;
+ }
+
+ /**
+ * Get the kernel id
+ */
+ get id(): string {
+ return this._id;
+ }
+
+ /**
+ * Dispose the kernel.
+ */
+ dispose(): void {
+ if (this.isDisposed) {
+ return;
+ }
+ this._isDisposed = true;
+ }
+
+ /**
+ * Register a new IFrame.
+ *
+ * @param iframe The IFrame to register.
+ */
+ async registerIFrame(iframe: HTMLIFrameElement): Promise {
+ await this._initIFrame(iframe);
+ for (const cell of this._cells) {
+ this._evalFunc(iframe.contentWindow, cell);
+ }
+ // call the preload and setup function
+ this._jsFunc(
+ iframe.contentWindow,
+ `
+ setTimeout(() => {
+ if (window.preload) window.preload();
+ else if (window.setup) window.setup();
+ window.loop();
+ }, 100);
+ `
+ );
+ this._iframes.push(iframe);
+ }
+
+ /**
+ * Handle an incoming message from the client.
+ *
+ * @param msg The message to handle
+ */
+ async handleMessage(msg: KernelMessage.IMessage): Promise {
+ // console.log(msg)
+ this._busy(msg);
+
+ const msgType = msg.header.msg_type;
+ switch (msgType) {
+ case 'kernel_info_request':
+ this._kernelInfo(msg);
+ break;
+ case 'execute_request':
+ this._executeRequest(msg);
+ break;
+ case 'complete_request':
+ this._complete(msg);
+ break;
+ default:
+ break;
+ }
+
+ this._idle(msg);
+ }
+
+ /**
+ * Send an 'idle' status message.
+ *
+ * @param parent The parent message
+ */
+ private _idle(parent: KernelMessage.IMessage): void {
+ const message = KernelMessage.createMessage({
+ msgType: 'status',
+ session: '',
+ parentHeader: parent.header,
+ channel: 'iopub',
+ content: {
+ execution_state: 'idle'
+ }
+ });
+ this._sendMessage(message);
+ }
+
+ /**
+ * Send a 'busy' status message.
+ *
+ * @param parent The parent message.
+ */
+ private _busy(parent: KernelMessage.IMessage): void {
+ const message = KernelMessage.createMessage({
+ msgType: 'status',
+ session: '',
+ parentHeader: parent.header,
+ channel: 'iopub',
+ content: {
+ execution_state: 'busy'
+ }
+ });
+ this._sendMessage(message);
+ }
+
+ /**
+ * Handle a kernel_info_request message
+ *
+ * @param parent The parent message.
+ */
+ private _kernelInfo(parent: KernelMessage.IMessage): void {
+ const content: KernelMessage.IInfoReply = {
+ implementation: 'pyp5',
+ implementation_version: '0.1.0',
+ language_info: {
+ codemirror_mode: {
+ name: 'python',
+ version: 3
+ },
+ file_extension: '.py',
+ mimetype: 'text/x-python',
+ name: 'python',
+ nbconvert_exporter: 'python',
+ pygments_lexer: 'ipython3',
+ version: '3.8'
+ },
+ protocol_version: '5.3',
+ status: 'ok',
+ banner: 'A pyp5js kernel powered by pyodide',
+ help_links: [
+ {
+ text: 'pyp5js Kernel',
+ url: 'https://github.com/jtpio/p5-notebook'
+ }
+ ]
+ };
+
+ const message = KernelMessage.createMessage({
+ msgType: 'kernel_info_reply',
+ channel: 'shell',
+ session: this._sessionId,
+ content
+ });
+
+ this._sendMessage(message);
+ }
+
+ /**
+ * Handle an `execute_request` message
+ *
+ * @param msg The parent message.
+ */
+ private _executeRequest(msg: KernelMessage.IMessage): void {
+ const parent = msg as KernelMessage.IExecuteRequestMsg;
+ this._execution_count++;
+
+ // store previous parent header
+ this._jsFunc(
+ this._iframe.contentWindow,
+ `window._parentHeader = ${JSON.stringify(parent.header)};`
+ );
+
+ this._executeInput(parent);
+ this._execute(parent);
+ }
+
+ /**
+ * Send an `execute_input` message.
+ *
+ * @param msg The parent message.
+ */
+ private _executeInput(msg: KernelMessage.IMessage): void {
+ const parent = msg as KernelMessage.IExecuteInputMsg;
+ const code = parent.content.code;
+ const message = KernelMessage.createMessage(
+ {
+ msgType: 'execute_input',
+ parentHeader: parent.header,
+ channel: 'iopub',
+ session: this._sessionId,
+ content: {
+ code,
+ execution_count: this._execution_count
+ }
+ }
+ );
+ this._sendMessage(message);
+ }
+
+ /**
+ * Execute the code.
+ *
+ * @param msg The parent message.
+ */
+ private _execute(msg: KernelMessage.IMessage): void {
+ const parent = msg as KernelMessage.IExecuteRequestMsg;
+ const code = parent.content.code;
+ try {
+ const result = this._eval(code);
+ this._executeResult(parent, {
+ data: {
+ 'text/plain': result
+ },
+ metadata: {}
+ });
+ this._execute_reply(parent, {
+ execution_count: this._execution_count,
+ status: 'ok',
+ user_expressions: {},
+ payload: []
+ });
+ } catch (e) {
+ const { name, stack, message } = e;
+ const error = {
+ ename: name,
+ evalue: message,
+ traceback: [stack]
+ };
+ this._error(parent, error);
+ this._execute_reply(parent, {
+ execution_count: this._execution_count,
+ status: 'error',
+ ...error
+ });
+ }
+ }
+
+ /**
+ * Send an `execute_result` message.
+ *
+ * @param msg The parent message.
+ * @param content The execut result content.
+ */
+ private _executeResult(
+ msg: KernelMessage.IMessage,
+ content: Pick<
+ KernelMessage.IExecuteResultMsg['content'],
+ 'data' | 'metadata'
+ >
+ ): void {
+ const message = KernelMessage.createMessage<
+ KernelMessage.IExecuteResultMsg
+ >({
+ msgType: 'execute_result',
+ parentHeader: msg.header,
+ channel: 'iopub',
+ session: this._sessionId,
+ content: {
+ ...content,
+ execution_count: this._execution_count
+ }
+ });
+ this._sendMessage(message);
+ }
+
+ /**
+ * Send an `error` message.
+ *
+ * @param msg The parent message.
+ * @param content The content for the execution error response.
+ */
+ private _error(
+ msg: KernelMessage.IMessage,
+ content: KernelMessage.IErrorMsg['content']
+ ): void {
+ const message = KernelMessage.createMessage({
+ msgType: 'error',
+ parentHeader: msg.header,
+ channel: 'iopub',
+ session: this._sessionId,
+ content
+ });
+ this._sendMessage(message);
+ }
+
+ /**
+ * Send an `execute_reply` message.
+ *
+ * @param msg The parent message.
+ * @param content The content for the execute reply.
+ */
+ private _execute_reply(
+ msg: KernelMessage.IMessage,
+ content: KernelMessage.IExecuteReplyMsg['content']
+ ): void {
+ const parent = msg as KernelMessage.IExecuteRequestMsg;
+ const message = KernelMessage.createMessage(
+ {
+ msgType: 'execute_reply',
+ channel: 'shell',
+ parentHeader: parent.header,
+ session: this._sessionId,
+ content
+ }
+ );
+ this._sendMessage(message);
+ }
+
+ /**
+ * Handle a stream event from the kernel
+ *
+ * @param parentHeader The parent header.
+ * @param content The stream content.
+ */
+ private _stream(
+ parentHeader: KernelMessage.IHeader,
+ content: KernelMessage.IStreamMsg['content']
+ ): void {
+ const message = KernelMessage.createMessage({
+ channel: 'iopub',
+ msgType: 'stream',
+ session: this._sessionId,
+ parentHeader,
+ content
+ });
+ this._sendMessage(message);
+ }
+
+ /**
+ * Handle an complete_request message
+ *
+ * @param msg The parent message.
+ */
+ private _complete(msg: KernelMessage.IMessage): void {
+ const parent = msg as KernelMessage.ICompleteRequestMsg;
+
+ // naive completion on window names only
+ // TODO: improve and move logic to the iframe
+ const vars = this._evalFunc(
+ this._iframe.contentWindow,
+ '["test"]'
+ ) as string[];
+ const { code, cursor_pos } = parent.content;
+ const words = code.slice(0, cursor_pos).match(/(\w+)$/) ?? [];
+ const word = words[0] ?? '';
+ const matches = vars.filter(v => v.startsWith(word));
+
+ const message = KernelMessage.createMessage<
+ KernelMessage.ICompleteReplyMsg
+ >({
+ msgType: 'complete_reply',
+ parentHeader: parent.header,
+ channel: 'shell',
+ session: this._sessionId,
+ content: {
+ matches,
+ cursor_start: cursor_pos - word.length,
+ cursor_end: cursor_pos,
+ metadata: {},
+ status: 'ok'
+ }
+ });
+
+ this._sendMessage(message);
+ }
+
+ /**
+ * Execute code in the kernel IFrame.
+ *
+ * @param code The code to execute.
+ */
+ private _eval(code: string): string {
+ // TODO: handle magics
+ if (code.startsWith('%show')) {
+ return '';
+ }
+ this._cells.push(code);
+ for (const frame of this._iframes) {
+ if (frame?.contentWindow) {
+ this._evalFunc(frame.contentWindow, code);
+ }
+ }
+ return this._evalFunc(this._iframe.contentWindow, code);
+ }
+
+ /**
+ * Create a new IFrame
+ *
+ * @param iframe The IFrame to initialize.
+ */
+ private async _initIFrame(
+ iframe: HTMLIFrameElement
+ ): Promise {
+ const delegate = new PromiseDelegate();
+ if (!iframe.contentWindow) {
+ delegate.reject('IFrame not ready');
+ return;
+ }
+ const doc = iframe.contentWindow.document;
+
+ // add pyodide
+ const pluginUrl = doc.createElement('script');
+ pluginUrl.textContent =
+ 'window.languagePluginUrl = "https://cdn.jsdelivr.net/pyodide/v0.15.0/full/"';
+ doc.head.appendChild(pluginUrl);
+
+ const script = doc.createElement('script');
+ script.src = 'https://cdn.jsdelivr.net/pyodide/v0.15.0/full/pyodide.js';
+ doc.head.appendChild(script);
+
+ delegate.resolve();
+ await delegate.promise;
+ return iframe;
+ }
+
+ private _id: string;
+ private _iframe: HTMLIFrameElement;
+ private _iframes: HTMLIFrameElement[] = [];
+ private _cells: string[] = [];
+ private _jsFunc = new Function('window', 'code', 'return window.eval(code);');
+ private _evalFunc = new Function(
+ 'window',
+ 'code',
+ 'return window.pyodide.runPython(code);'
+ );
+ private _execution_count = 0;
+ private _sessionId: string;
+ private _isDisposed = false;
+ private _sendMessage: (msg: KernelMessage.IMessage) => void;
+}
+
+/**
+ * A namespace for IFrameKernel statics.
+ */
+export namespace IFrameKernel {
+ /**
+ * The instantiation options for an IFrameKernel.
+ */
+ export interface IOptions {
+ /**
+ * The kernel id.
+ */
+ id: string;
+
+ /**
+ * The session id.
+ */
+ sessionId: string;
+
+ /**
+ * The method to send messages back to the server.
+ */
+ sendMessage: (msg: KernelMessage.IMessage) => void;
+ }
+}
diff --git a/src/server/kernelspecs.ts b/src/server/kernelspecs.ts
index 41b9dea..6da655c 100644
--- a/src/server/kernelspecs.ts
+++ b/src/server/kernelspecs.ts
@@ -71,6 +71,24 @@ namespace Private {
'logo-32x32': '/kernelspecs/p5/logo-32x32.png',
'logo-64x64': '/build/resources/p5js-square-logo.svg'
}
+ },
+ pyp5: {
+ name: 'pyp5',
+ display_name: 'pyp5js',
+ language: 'python',
+ argv: [],
+ spec: {
+ argv: [],
+ env: {},
+ display_name: 'pyp5js',
+ language: 'python',
+ interrupt_mode: 'message',
+ metadata: {}
+ },
+ resources: {
+ 'logo-32x32': '/kernelspecs/p5/logo-32x32.png',
+ 'logo-64x64': '/build/resources/pyp5js.svg'
+ }
}
}
};
diff --git a/src/server/sessions.ts b/src/server/sessions.ts
index fbb505d..21b54c0 100644
--- a/src/server/sessions.ts
+++ b/src/server/sessions.ts
@@ -53,8 +53,9 @@ export class Sessions implements IJupyterServer.IRoutable {
*/
startNew(options: Session.IModel): Session.IModel {
const { path, name } = options;
+ const kernelName = options.kernel?.name;
const id = options.id ?? UUID.uuid4();
- const kernel = this._kernels.startNew(id);
+ const kernel = this._kernels.startNew({ id, name: kernelName });
const session: Session.IModel = {
id,
path: path ?? name,
diff --git a/src/tokens.ts b/src/tokens.ts
index 4ac7c66..e19adb7 100644
--- a/src/tokens.ts
+++ b/src/tokens.ts
@@ -8,10 +8,15 @@ export namespace IJupyterServer {
* An interface for a IFramed based kernel.
*/
export interface IKernelIFrame {
+ /**
+ * Dispose the kernel.
+ */
+ dispose(): void;
+
/**
* Register an IFrame from the frontend.
*/
- registerIFrame(iframe: HTMLIFrameElement): void;
+ registerIFrame(iframe: HTMLIFrameElement): Promise;
/**
* Handle an incoming message from the server.
diff --git a/stage.js b/stage.js
index 87d967c..8b3a895 100644
--- a/stage.js
+++ b/stage.js
@@ -9,7 +9,8 @@ fs.ensureDirSync(dest);
'index.html',
'favicon.ico',
'./build/bundle.js',
- './build/resources/p5js-square-logo.svg'
+ './build/resources/p5js-square-logo.svg',
+ './build/resources/pyp5js.svg'
].forEach(f => {
const src = path.resolve(basePath, f);
const target = path.resolve(dest, f);