diff --git a/README.md b/README.md index 7a0fc34..6518873 100644 --- a/README.md +++ b/README.md @@ -2,26 +2,47 @@ [![Github Actions Status](https://github.com/ianthomas23/jupyterlite-terminal/workflows/Build/badge.svg)](https://github.com/ianthomas23/jupyterlite-terminal/actions/workflows/build.yml) -A terminal for JupyterLite +A terminal for JupyterLite. + +⚠️ This extension is still in development and not yet ready for general use. ⚠️ + +![a screenshot showing a terminal running in JupyterLite](https://github.com/ianthomas23/jupyterlite-terminal/assets/591645/1b4ff620-e8f2-4abf-b608-6badd66370ac) ## Requirements -- JupyterLab >= 4.0.0 +- JupyterLite >= 0.4.0 ## Install To install the extension, execute: ```bash -pip install jupyterlite_terminal +pip install jupyterlite-terminal +``` + +You will also need to install the JupyterLite CLI: + +```bash +python -m pip install --pre jupyterlite-core ``` -## Uninstall +## Usage + +After installing `jupyterlite-core` and `jupyterlite-terminal`, create a `jupyter-lite.json` file with the following content to activate the terminal extension: -To remove the extension, execute: +```json +{ + "jupyter-lite-schema-version": 0, + "jupyter-config-data": { + "terminalsAvailable": true + } +} +``` + +Then build a new JupyterLite site: ```bash -pip uninstall jupyterlite_terminal +jupyter lite build ``` ## Contributing @@ -54,44 +75,18 @@ jlpm watch jupyter lab ``` -With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). - -By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: +Then build a JupyterLite distribution with the extension installed: ```bash -jupyter lab build --minimize=False +jupyter lite build ``` -### Development uninstall +And serve it: ```bash -pip uninstall jupyterlite_terminal +jupyter lite serve ``` -In development mode, you will also need to remove the symlink created by `jupyter labextension develop` -command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` -folder is located. Then you can remove the symlink named `jupyterlite-terminal` within that folder. - -### Testing the extension - -#### Frontend tests - -This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. - -To execute them, execute: - -```sh -jlpm -jlpm test -``` - -#### Integration tests - -This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). -More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. - -More information are provided within the [ui-tests](./ui-tests/README.md) README. - ### Packaging the extension See [RELEASE](RELEASE.md) diff --git a/package.json b/package.json index 02eaa74..68154ed 100644 --- a/package.json +++ b/package.json @@ -57,11 +57,13 @@ "watch:labextension": "jupyter labextension watch ." }, "dependencies": { + "@ianthomas23/cockle": "^0.0.2", "@jupyterlab/services": "^7.2.0", "@jupyterlab/terminal": "^4.2.0", "@jupyterlab/terminal-extension": "^4.2.0", "@jupyterlite/server": "^0.3.0", - "@lumino/coreutils": "^2.1.2" + "@lumino/coreutils": "^2.1.2", + "mock-socket": "^9.3.1" }, "devDependencies": { "@jupyterlab/builder": "^4.0.0", diff --git a/src/index.ts b/src/index.ts index 7f0584d..cf421da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ import { JupyterLiteServerPlugin, Router } from '@jupyterlite/server'; -import { ITerminalTracker } from '@jupyterlab/terminal'; import { ITerminals } from './tokens'; import { Terminals } from './terminals'; @@ -18,15 +17,12 @@ const terminalsPlugin: JupyterLiteServerPlugin = { id: 'jupyterlite-terminal:plugin', description: 'A terminal for JupyterLite', autoStart: true, - requires: [ITerminalTracker], provides: ITerminals, - activate: async (app: JupyterLiteServer, tracker: ITerminalTracker) => { + activate: async (app: JupyterLiteServer) => { console.log( 'JupyterLab extension jupyterlite-terminal:plugin is activated!' ); - console.log('==> ITerminalTracker', tracker); - const { serviceManager } = app; const { contents, serverSettings, terminals } = serviceManager; console.log('terminals available:', terminals.isAvailable()); diff --git a/src/terminal.ts b/src/terminal.ts index 3dc9a45..c81854e 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -1,6 +1,8 @@ // Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. +import { JupyterFileSystem, Shell, IFileSystem } from '@ianthomas23/cockle'; + import { JSONPrimitive } from '@lumino/coreutils'; import { @@ -16,6 +18,8 @@ export class Terminal implements ITerminal { */ constructor(options: ITerminal.IOptions) { this._name = options.name; + this._fs = new JupyterFileSystem(options.contentsManager); + console.log('==> new Terminal', this._name, this._fs); } /** @@ -28,14 +32,34 @@ export class Terminal implements ITerminal { async wsConnect(url: string) { console.log('==> Terminal.wsConnect', url); - const server = new WebSocketServer(url, { mock: false }); + // const server = new WebSocketServer(url, { mock: false }); + const server = new WebSocketServer(url); server.on('connection', async (socket: WebSocketClient) => { console.log('==> server connection', this, socket); + const outputCallback = async (output: string) => { + console.log('==> recv from shell:', output); + const ret = JSON.stringify(['stdout', output]); + socket.send(ret); + }; + + this._shell = new Shell(this._fs, outputCallback); + console.log('==> shell', this._shell); + socket.on('message', async (message: any) => { const data = JSON.parse(message) as JSONPrimitive[]; console.log('==> socket message', data); + const message_type = data[0]; + const content = data.slice(1); + + if (message_type === 'stdin') { + await this._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 this._shell!.setSize(rows, columns); + } }); socket.on('close', async () => { @@ -50,8 +74,12 @@ export class Terminal implements ITerminal { const res = JSON.stringify(['setup']); console.log('==> Returning handshake via socket', res); socket.send(res); + + await this._shell!.start(); }); } private _name: string; + private _fs: IFileSystem; + private _shell?: Shell; } diff --git a/yarn.lock b/yarn.lock index f15893e..65de455 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,6 +1746,15 @@ __metadata: languageName: node linkType: hard +"@ianthomas23/cockle@npm:^0.0.2": + version: 0.0.2 + resolution: "@ianthomas23/cockle@npm:0.0.2" + dependencies: + "@jupyterlab/services": ^7.1.6 + checksum: 7426122ce9e05cebf6db3c88a06b068eafa65dc8a6ba72384c6a9a62f95d13ebc3a3cb865ecaa6989a8809d0a0fc17155689be7d6067d5f3ffb4e0dbb0a32d33 + languageName: node + linkType: hard + "@isaacs/cliui@npm:^8.0.2": version: 8.0.2 resolution: "@isaacs/cliui@npm:8.0.2" @@ -2341,6 +2350,20 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/coreutils@npm:^6.2.1": + version: 6.2.1 + resolution: "@jupyterlab/coreutils@npm:6.2.1" + dependencies: + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + minimist: ~1.2.0 + path-browserify: ^1.0.0 + url-parse: ~1.5.4 + checksum: c8167bd8d4472471297e5669d6b3ee7c9d5c1246e8413680713b15f8a81926d2c97bc6a3c0b26c16603b197b412e01b443cc74b02a3676adea5690aac41964be + languageName: node + linkType: hard + "@jupyterlab/coreutils@npm:~6.1.5": version: 6.1.8 resolution: "@jupyterlab/coreutils@npm:6.1.8" @@ -2518,6 +2541,15 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/nbformat@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/nbformat@npm:4.2.1" + dependencies: + "@lumino/coreutils": ^2.1.2 + checksum: 192167e2a9019bf91e1e7088c9eaaae7b1037f5e7b5db15b97687b052323e6e75913b301ca7a9783d0e59aa36f18ddff90fc71a90a8153e0c89e32fd92b2519c + languageName: node + linkType: hard + "@jupyterlab/nbformat@npm:~4.1.5": version: 4.1.8 resolution: "@jupyterlab/nbformat@npm:4.1.8" @@ -2662,6 +2694,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/services@npm:^7.1.6": + version: 7.2.1 + resolution: "@jupyterlab/services@npm:7.2.1" + dependencies: + "@jupyter/ydoc": ^2.0.1 + "@jupyterlab/coreutils": ^6.2.1 + "@jupyterlab/nbformat": ^4.2.1 + "@jupyterlab/settingregistry": ^4.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/polling": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + ws: ^8.11.0 + checksum: f07be2f3a174466c17ab5c22f8ef622fc623e8c61f2220b8bfb465a263971313cb9129e84bba32606e6ab7d1e0be3a9754b97f98e173e9c95eaf0b1c6cd8110a + languageName: node + linkType: hard + "@jupyterlab/services@npm:^7.2.0": version: 7.2.0 resolution: "@jupyterlab/services@npm:7.2.0" @@ -2719,6 +2770,25 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/settingregistry@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/settingregistry@npm:4.2.1" + dependencies: + "@jupyterlab/nbformat": ^4.2.1 + "@jupyterlab/statedb": ^4.2.1 + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/signaling": ^2.1.2 + "@rjsf/utils": ^5.13.4 + ajv: ^8.12.0 + json5: ^2.2.3 + peerDependencies: + react: ">=16" + checksum: 794e5ecde19a40e1b95c0d636eed7b56bbdc46857c8f3b4ef446c1bc90e8ea660c2ccf8f36a238bc312002f106a5a8522bb057742d9c0d674b2974ef21a786d7 + languageName: node + linkType: hard + "@jupyterlab/settingregistry@npm:~4.1.5": version: 4.1.8 resolution: "@jupyterlab/settingregistry@npm:4.1.8" @@ -2751,6 +2821,19 @@ __metadata: languageName: node linkType: hard +"@jupyterlab/statedb@npm:^4.2.1": + version: 4.2.1 + resolution: "@jupyterlab/statedb@npm:4.2.1" + dependencies: + "@lumino/commands": ^2.3.0 + "@lumino/coreutils": ^2.1.2 + "@lumino/disposable": ^2.1.2 + "@lumino/properties": ^2.0.1 + "@lumino/signaling": ^2.1.2 + checksum: 51e07db85269883bcd58fc5ba890db122e260e8d1ce4046f0b188453726694c2d909f27ca069ee3cd6944a93d70fcb8360074f87cdb13d611af2e24f6b14af30 + languageName: node + linkType: hard + "@jupyterlab/statedb@npm:~4.1.5": version: 4.1.8 resolution: "@jupyterlab/statedb@npm:4.1.8" @@ -7612,6 +7695,7 @@ __metadata: version: 0.0.0-use.local resolution: "jupyterlite-terminal@workspace:." dependencies: + "@ianthomas23/cockle": ^0.0.2 "@jupyterlab/builder": ^4.0.0 "@jupyterlab/services": ^7.2.0 "@jupyterlab/terminal": ^4.2.0 @@ -7630,6 +7714,7 @@ __metadata: eslint-config-prettier: ^8.8.0 eslint-plugin-prettier: ^5.0.0 jest: ^29.2.0 + mock-socket: ^9.3.1 npm-run-all: ^4.1.5 prettier: ^3.0.0 rimraf: ^5.0.1 @@ -8223,7 +8308,7 @@ __metadata: languageName: node linkType: hard -"mock-socket@npm:^9.1.0": +"mock-socket@npm:^9.1.0, mock-socket@npm:^9.3.1": version: 9.3.1 resolution: "mock-socket@npm:9.3.1" checksum: cb2dde4fc5dde280dd5ccb78eaaa223382ee16437f46b86558017655584ad08c22e733bde2dd5cc86927def506b6caeb0147e3167b9a62d70d5cf19d44103853