@jupyterlite/terminal 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +30 -0
- package/README.md +92 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +49 -0
- package/lib/terminal.d.ts +15 -0
- package/lib/terminal.js +60 -0
- package/lib/terminals.d.ts +23 -0
- package/lib/terminals.js +47 -0
- package/lib/tokens.d.ts +43 -0
- package/lib/tokens.js +7 -0
- package/package.json +204 -0
- package/src/__tests__/jupyterlite_terminal.spec.ts +9 -0
- package/src/index.ts +69 -0
- package/src/terminal.ts +85 -0
- package/src/terminals.ts +64 -0
- package/src/tokens.ts +55 -0
- package/style/base.css +5 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c), Ian Thomas
|
|
4
|
+
Copyright (c), JupyterLite Contributors
|
|
5
|
+
All rights reserved.
|
|
6
|
+
|
|
7
|
+
Redistribution and use in source and binary forms, with or without
|
|
8
|
+
modification, are permitted provided that the following conditions are met:
|
|
9
|
+
|
|
10
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
11
|
+
list of conditions and the following disclaimer.
|
|
12
|
+
|
|
13
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
14
|
+
this list of conditions and the following disclaimer in the documentation
|
|
15
|
+
and/or other materials provided with the distribution.
|
|
16
|
+
|
|
17
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
18
|
+
contributors may be used to endorse or promote products derived from
|
|
19
|
+
this software without specific prior written permission.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
22
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
23
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
24
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
25
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
26
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
27
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
28
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
29
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
30
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# jupyterlite_terminal
|
|
2
|
+
|
|
3
|
+
[](https://github.com/jupyterlite/terminal/actions/workflows/build.yml)
|
|
4
|
+
|
|
5
|
+
A terminal for JupyterLite.
|
|
6
|
+
|
|
7
|
+
⚠️ This extension is still in development and not yet ready for general use. ⚠️
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- JupyterLite >= 0.4.0
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
To install the extension, execute:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install jupyterlite-terminal
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You will also need to install the JupyterLite CLI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m pip install --pre jupyterlite-core
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
After installing `jupyterlite-core` and `jupyterlite-terminal`, create a `jupyter-lite.json` file with the following content to activate the terminal extension:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"jupyter-lite-schema-version": 0,
|
|
36
|
+
"jupyter-config-data": {
|
|
37
|
+
"terminalsAvailable": true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then build a new JupyterLite site:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
jupyter lite build
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Contributing
|
|
49
|
+
|
|
50
|
+
### Development install
|
|
51
|
+
|
|
52
|
+
Note: You will need NodeJS to build the extension package.
|
|
53
|
+
|
|
54
|
+
The `jlpm` command is JupyterLab's pinned version of
|
|
55
|
+
[yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
|
|
56
|
+
`yarn` or `npm` in lieu of `jlpm` below.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# Clone the repo to your local environment
|
|
60
|
+
# Change directory to the jupyterlite_terminal directory
|
|
61
|
+
# Install package in development mode
|
|
62
|
+
pip install -e "."
|
|
63
|
+
# Link your development version of the extension with JupyterLab
|
|
64
|
+
jupyter labextension develop . --overwrite
|
|
65
|
+
# Rebuild extension Typescript source after making changes
|
|
66
|
+
jlpm build
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Watch the source directory in one terminal, automatically rebuilding when needed
|
|
73
|
+
jlpm watch
|
|
74
|
+
# Run JupyterLab in another terminal
|
|
75
|
+
jupyter lab
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Then build a JupyterLite distribution with the extension installed:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
jupyter lite build
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
And serve it:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
jupyter lite serve
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Packaging the extension
|
|
91
|
+
|
|
92
|
+
See [RELEASE](RELEASE.md)
|
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { ITerminals } from './tokens';
|
|
4
|
+
import { Terminals } from './terminals';
|
|
5
|
+
/**
|
|
6
|
+
* The terminals service plugin.
|
|
7
|
+
*/
|
|
8
|
+
const terminalsPlugin = {
|
|
9
|
+
id: '@jupyterlite/terminal:plugin',
|
|
10
|
+
description: 'A terminal for JupyterLite',
|
|
11
|
+
autoStart: true,
|
|
12
|
+
provides: ITerminals,
|
|
13
|
+
activate: async (app) => {
|
|
14
|
+
console.log('JupyterLab extension @jupyterlite/terminal:plugin is activated!');
|
|
15
|
+
const { serviceManager } = app;
|
|
16
|
+
const { contents, serverSettings, terminals } = serviceManager;
|
|
17
|
+
console.log('terminals available:', terminals.isAvailable());
|
|
18
|
+
console.log('terminals ready:', terminals.isReady); // Not ready
|
|
19
|
+
console.log('terminals active:', terminals.isActive);
|
|
20
|
+
// Not sure this is necessary?
|
|
21
|
+
await terminals.ready;
|
|
22
|
+
console.log('terminals ready after await:', terminals.isReady); // Ready
|
|
23
|
+
return new Terminals(serverSettings.wsUrl, contents);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A plugin providing the routes for the terminals service
|
|
28
|
+
*/
|
|
29
|
+
const terminalsRoutesPlugin = {
|
|
30
|
+
id: '@jupyterlite/terminal:routes-plugin',
|
|
31
|
+
autoStart: true,
|
|
32
|
+
requires: [ITerminals],
|
|
33
|
+
activate: (app, terminals) => {
|
|
34
|
+
console.log('JupyterLab extension @jupyterlite/terminal:routes-plugin is activated!', terminals);
|
|
35
|
+
// GET /api/terminals - List the running terminals
|
|
36
|
+
app.router.get('/api/terminals', async (req) => {
|
|
37
|
+
const res = terminals.list();
|
|
38
|
+
// Should return last_activity for each too,
|
|
39
|
+
return new Response(JSON.stringify(res));
|
|
40
|
+
});
|
|
41
|
+
// POST /api/terminals - Start a terminal
|
|
42
|
+
app.router.post('/api/terminals', async (req) => {
|
|
43
|
+
const res = await terminals.startNew();
|
|
44
|
+
// Should return last_activity too.
|
|
45
|
+
return new Response(JSON.stringify(res));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
export default [terminalsPlugin, terminalsRoutesPlugin];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { ITerminal } from './tokens';
|
|
2
|
+
export declare class Terminal implements ITerminal {
|
|
3
|
+
/**
|
|
4
|
+
* Construct a new Terminal.
|
|
5
|
+
*/
|
|
6
|
+
constructor(options: ITerminal.IOptions);
|
|
7
|
+
/**
|
|
8
|
+
* Get the name of the terminal.
|
|
9
|
+
*/
|
|
10
|
+
get name(): string;
|
|
11
|
+
wsConnect(url: string): Promise<void>;
|
|
12
|
+
private _name;
|
|
13
|
+
private _fs;
|
|
14
|
+
private _shell?;
|
|
15
|
+
}
|
package/lib/terminal.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { JupyterFileSystem, Shell } from '@jupyterlite/cockle';
|
|
4
|
+
import { Server as WebSocketServer } from 'mock-socket';
|
|
5
|
+
export class Terminal {
|
|
6
|
+
/**
|
|
7
|
+
* Construct a new Terminal.
|
|
8
|
+
*/
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this._name = options.name;
|
|
11
|
+
this._fs = new JupyterFileSystem(options.contentsManager);
|
|
12
|
+
console.log('==> new Terminal', this._name, this._fs);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Get the name of the terminal.
|
|
16
|
+
*/
|
|
17
|
+
get name() {
|
|
18
|
+
return this._name;
|
|
19
|
+
}
|
|
20
|
+
async wsConnect(url) {
|
|
21
|
+
console.log('==> Terminal.wsConnect', url);
|
|
22
|
+
// const server = new WebSocketServer(url, { mock: false });
|
|
23
|
+
const server = new WebSocketServer(url);
|
|
24
|
+
server.on('connection', async (socket) => {
|
|
25
|
+
console.log('==> server connection', this, socket);
|
|
26
|
+
const outputCallback = async (output) => {
|
|
27
|
+
console.log('==> recv from shell:', output);
|
|
28
|
+
const ret = JSON.stringify(['stdout', output]);
|
|
29
|
+
socket.send(ret);
|
|
30
|
+
};
|
|
31
|
+
this._shell = new Shell(this._fs, outputCallback);
|
|
32
|
+
console.log('==> shell', this._shell);
|
|
33
|
+
socket.on('message', async (message) => {
|
|
34
|
+
const data = JSON.parse(message);
|
|
35
|
+
console.log('==> socket message', data);
|
|
36
|
+
const message_type = data[0];
|
|
37
|
+
const content = data.slice(1);
|
|
38
|
+
if (message_type === 'stdin') {
|
|
39
|
+
await this._shell.input(content[0]);
|
|
40
|
+
}
|
|
41
|
+
else if (message_type === 'set_size') {
|
|
42
|
+
const rows = content[0];
|
|
43
|
+
const columns = content[1];
|
|
44
|
+
await this._shell.setSize(rows, columns);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
socket.on('close', async () => {
|
|
48
|
+
console.log('==> socket close');
|
|
49
|
+
});
|
|
50
|
+
socket.on('error', async () => {
|
|
51
|
+
console.log('==> socket error');
|
|
52
|
+
});
|
|
53
|
+
// Return handshake.
|
|
54
|
+
const res = JSON.stringify(['setup']);
|
|
55
|
+
console.log('==> Returning handshake via socket', res);
|
|
56
|
+
socket.send(res);
|
|
57
|
+
await this._shell.start();
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Contents, TerminalAPI } from '@jupyterlab/services';
|
|
2
|
+
import { ITerminals } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* A class to handle requests to /api/terminals
|
|
5
|
+
*/
|
|
6
|
+
export declare class Terminals implements ITerminals {
|
|
7
|
+
/**
|
|
8
|
+
* Construct a new Terminals object.
|
|
9
|
+
*/
|
|
10
|
+
constructor(wsUrl: string, contentsManager: Contents.IManager);
|
|
11
|
+
/**
|
|
12
|
+
* List the running terminals.
|
|
13
|
+
*/
|
|
14
|
+
list(): Promise<TerminalAPI.IModel[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Start a new kernel.
|
|
17
|
+
*/
|
|
18
|
+
startNew(): Promise<TerminalAPI.IModel>;
|
|
19
|
+
private _nextAvailableName;
|
|
20
|
+
private _wsUrl;
|
|
21
|
+
private _contentsManager;
|
|
22
|
+
private _terminals;
|
|
23
|
+
}
|
package/lib/terminals.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { Terminal } from './terminal';
|
|
4
|
+
/**
|
|
5
|
+
* A class to handle requests to /api/terminals
|
|
6
|
+
*/
|
|
7
|
+
export class Terminals {
|
|
8
|
+
/**
|
|
9
|
+
* Construct a new Terminals object.
|
|
10
|
+
*/
|
|
11
|
+
constructor(wsUrl, contentsManager) {
|
|
12
|
+
this._terminals = new Map();
|
|
13
|
+
this._wsUrl = wsUrl;
|
|
14
|
+
this._contentsManager = contentsManager;
|
|
15
|
+
console.log('==> Terminals.constructor', this._wsUrl, this._contentsManager);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* List the running terminals.
|
|
19
|
+
*/
|
|
20
|
+
async list() {
|
|
21
|
+
const ret = [...this._terminals.values()].map(terminal => ({
|
|
22
|
+
name: terminal.name
|
|
23
|
+
}));
|
|
24
|
+
console.log('==> Terminals.list', ret);
|
|
25
|
+
return ret;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Start a new kernel.
|
|
29
|
+
*/
|
|
30
|
+
async startNew() {
|
|
31
|
+
const name = this._nextAvailableName();
|
|
32
|
+
console.log('==> Terminals.new', name);
|
|
33
|
+
const term = new Terminal({ name, contentsManager: this._contentsManager });
|
|
34
|
+
this._terminals.set(name, term);
|
|
35
|
+
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
36
|
+
await term.wsConnect(url);
|
|
37
|
+
return { name };
|
|
38
|
+
}
|
|
39
|
+
_nextAvailableName() {
|
|
40
|
+
for (let i = 1;; ++i) {
|
|
41
|
+
const name = `${i}`;
|
|
42
|
+
if (!this._terminals.has(name)) {
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
package/lib/tokens.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Contents, TerminalAPI } from '@jupyterlab/services';
|
|
2
|
+
import { Token } from '@lumino/coreutils';
|
|
3
|
+
/**
|
|
4
|
+
* The token for the Terminals service.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ITerminals: Token<ITerminals>;
|
|
7
|
+
/**
|
|
8
|
+
* An interface for the Terminals service.
|
|
9
|
+
*/
|
|
10
|
+
export interface ITerminals {
|
|
11
|
+
/**
|
|
12
|
+
* List the running terminals.
|
|
13
|
+
*/
|
|
14
|
+
list: () => Promise<TerminalAPI.IModel[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Start a new kernel.
|
|
17
|
+
*/
|
|
18
|
+
startNew: () => Promise<TerminalAPI.IModel>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* An interface for a server-side terminal running in the browser.
|
|
22
|
+
*/
|
|
23
|
+
export interface ITerminal {
|
|
24
|
+
/**
|
|
25
|
+
* The name of the server-side terminal.
|
|
26
|
+
*/
|
|
27
|
+
readonly name: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A namespace for ITerminal statics.
|
|
31
|
+
*/
|
|
32
|
+
export declare namespace ITerminal {
|
|
33
|
+
/**
|
|
34
|
+
* The instantiation options for an ITerminal.
|
|
35
|
+
*/
|
|
36
|
+
interface IOptions {
|
|
37
|
+
/**
|
|
38
|
+
* The name of the terminal.
|
|
39
|
+
*/
|
|
40
|
+
name: string;
|
|
41
|
+
contentsManager: Contents.IManager;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
import { Token } from '@lumino/coreutils';
|
|
4
|
+
/**
|
|
5
|
+
* The token for the Terminals service.
|
|
6
|
+
*/
|
|
7
|
+
export const ITerminals = new Token('@jupyterlite/terminal:ITerminals');
|
package/package.json
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jupyterlite/terminal",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A terminal for JupyterLite",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jupyter",
|
|
7
|
+
"jupyterlab",
|
|
8
|
+
"jupyterlite",
|
|
9
|
+
"jupyterlite-extension"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/jupyterlite/terminal",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/jupyterlite/terminal/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "BSD-3-Clause",
|
|
16
|
+
"author": {
|
|
17
|
+
"name": "JupyterLite Contributors",
|
|
18
|
+
"email": ""
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
|
|
22
|
+
"style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
|
|
23
|
+
"src/**/*.{ts,tsx}"
|
|
24
|
+
],
|
|
25
|
+
"main": "lib/index.js",
|
|
26
|
+
"types": "lib/index.d.ts",
|
|
27
|
+
"style": "style/index.css",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/jupyterlite/terminal.git"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "jlpm build:lib && jlpm build:labextension:dev",
|
|
34
|
+
"build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
|
|
35
|
+
"build:labextension": "jupyter labextension build .",
|
|
36
|
+
"build:labextension:dev": "jupyter labextension build --development True .",
|
|
37
|
+
"build:lib": "tsc --sourceMap",
|
|
38
|
+
"build:lib:prod": "tsc",
|
|
39
|
+
"clean": "jlpm clean:lib",
|
|
40
|
+
"clean:lib": "rimraf lib tsconfig.tsbuildinfo",
|
|
41
|
+
"clean:lintcache": "rimraf .eslintcache .stylelintcache",
|
|
42
|
+
"clean:labextension": "rimraf jupyterlite_terminal/labextension jupyterlite_terminal/_version.py",
|
|
43
|
+
"clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
|
|
44
|
+
"eslint": "jlpm eslint:check --fix",
|
|
45
|
+
"eslint:check": "eslint . --cache --ext .ts,.tsx",
|
|
46
|
+
"install:extension": "jlpm build",
|
|
47
|
+
"lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
|
|
48
|
+
"lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
|
|
49
|
+
"prettier": "jlpm prettier:base --write --list-different",
|
|
50
|
+
"prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
|
|
51
|
+
"prettier:check": "jlpm prettier:base --check",
|
|
52
|
+
"stylelint": "jlpm stylelint:check --fix",
|
|
53
|
+
"stylelint:check": "stylelint --cache \"style/**/*.css\"",
|
|
54
|
+
"test": "jest --coverage",
|
|
55
|
+
"watch": "run-p watch:src watch:labextension",
|
|
56
|
+
"watch:src": "tsc -w --sourceMap",
|
|
57
|
+
"watch:labextension": "jupyter labextension watch ."
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@jupyterlab/services": "^7.2.0",
|
|
61
|
+
"@jupyterlab/terminal": "^4.2.0",
|
|
62
|
+
"@jupyterlab/terminal-extension": "^4.2.0",
|
|
63
|
+
"@jupyterlite/cockle": "^0.0.3",
|
|
64
|
+
"@jupyterlite/server": "^0.3.0",
|
|
65
|
+
"@lumino/coreutils": "^2.1.2",
|
|
66
|
+
"mock-socket": "^9.3.1"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@jupyterlab/builder": "^4.0.0",
|
|
70
|
+
"@jupyterlab/testutils": "^4.0.0",
|
|
71
|
+
"@types/jest": "^29.2.0",
|
|
72
|
+
"@types/json-schema": "^7.0.11",
|
|
73
|
+
"@types/react": "^18.0.26",
|
|
74
|
+
"@types/react-addons-linked-state-mixin": "^0.14.22",
|
|
75
|
+
"@typescript-eslint/eslint-plugin": "^6.1.0",
|
|
76
|
+
"@typescript-eslint/parser": "^6.1.0",
|
|
77
|
+
"css-loader": "^6.7.1",
|
|
78
|
+
"eslint": "^8.36.0",
|
|
79
|
+
"eslint-config-prettier": "^8.8.0",
|
|
80
|
+
"eslint-plugin-prettier": "^5.0.0",
|
|
81
|
+
"jest": "^29.2.0",
|
|
82
|
+
"npm-run-all": "^4.1.5",
|
|
83
|
+
"prettier": "^3.0.0",
|
|
84
|
+
"rimraf": "^5.0.1",
|
|
85
|
+
"source-map-loader": "^1.0.2",
|
|
86
|
+
"style-loader": "^3.3.1",
|
|
87
|
+
"stylelint": "^15.10.1",
|
|
88
|
+
"stylelint-config-recommended": "^13.0.0",
|
|
89
|
+
"stylelint-config-standard": "^34.0.0",
|
|
90
|
+
"stylelint-csstree-validator": "^3.0.0",
|
|
91
|
+
"stylelint-prettier": "^4.0.0",
|
|
92
|
+
"typescript": "~5.0.2",
|
|
93
|
+
"yjs": "^13.5.0"
|
|
94
|
+
},
|
|
95
|
+
"sideEffects": [
|
|
96
|
+
"style/*.css",
|
|
97
|
+
"style/index.js"
|
|
98
|
+
],
|
|
99
|
+
"styleModule": "style/index.js",
|
|
100
|
+
"publishConfig": {
|
|
101
|
+
"access": "public"
|
|
102
|
+
},
|
|
103
|
+
"jupyterlab": {
|
|
104
|
+
"extension": true,
|
|
105
|
+
"outputDir": "jupyterlite_terminal/labextension"
|
|
106
|
+
},
|
|
107
|
+
"jupyterlite": {
|
|
108
|
+
"liteExtension": true
|
|
109
|
+
},
|
|
110
|
+
"eslintIgnore": [
|
|
111
|
+
"node_modules",
|
|
112
|
+
"dist",
|
|
113
|
+
"coverage",
|
|
114
|
+
"**/*.d.ts",
|
|
115
|
+
"tests",
|
|
116
|
+
"**/__tests__",
|
|
117
|
+
"ui-tests"
|
|
118
|
+
],
|
|
119
|
+
"eslintConfig": {
|
|
120
|
+
"extends": [
|
|
121
|
+
"eslint:recommended",
|
|
122
|
+
"plugin:@typescript-eslint/eslint-recommended",
|
|
123
|
+
"plugin:@typescript-eslint/recommended",
|
|
124
|
+
"plugin:prettier/recommended"
|
|
125
|
+
],
|
|
126
|
+
"parser": "@typescript-eslint/parser",
|
|
127
|
+
"parserOptions": {
|
|
128
|
+
"project": "tsconfig.json",
|
|
129
|
+
"sourceType": "module"
|
|
130
|
+
},
|
|
131
|
+
"plugins": [
|
|
132
|
+
"@typescript-eslint"
|
|
133
|
+
],
|
|
134
|
+
"rules": {
|
|
135
|
+
"@typescript-eslint/naming-convention": [
|
|
136
|
+
"error",
|
|
137
|
+
{
|
|
138
|
+
"selector": "interface",
|
|
139
|
+
"format": [
|
|
140
|
+
"PascalCase"
|
|
141
|
+
],
|
|
142
|
+
"custom": {
|
|
143
|
+
"regex": "^I[A-Z]",
|
|
144
|
+
"match": true
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"@typescript-eslint/no-unused-vars": [
|
|
149
|
+
"warn",
|
|
150
|
+
{
|
|
151
|
+
"args": "none"
|
|
152
|
+
}
|
|
153
|
+
],
|
|
154
|
+
"@typescript-eslint/no-explicit-any": "off",
|
|
155
|
+
"@typescript-eslint/no-namespace": "off",
|
|
156
|
+
"@typescript-eslint/no-use-before-define": "off",
|
|
157
|
+
"@typescript-eslint/quotes": [
|
|
158
|
+
"error",
|
|
159
|
+
"single",
|
|
160
|
+
{
|
|
161
|
+
"avoidEscape": true,
|
|
162
|
+
"allowTemplateLiterals": false
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"curly": [
|
|
166
|
+
"error",
|
|
167
|
+
"all"
|
|
168
|
+
],
|
|
169
|
+
"eqeqeq": "error",
|
|
170
|
+
"prefer-arrow-callback": "error"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
"prettier": {
|
|
174
|
+
"singleQuote": true,
|
|
175
|
+
"trailingComma": "none",
|
|
176
|
+
"arrowParens": "avoid",
|
|
177
|
+
"endOfLine": "auto",
|
|
178
|
+
"overrides": [
|
|
179
|
+
{
|
|
180
|
+
"files": "package.json",
|
|
181
|
+
"options": {
|
|
182
|
+
"tabWidth": 4
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
},
|
|
187
|
+
"stylelint": {
|
|
188
|
+
"extends": [
|
|
189
|
+
"stylelint-config-recommended",
|
|
190
|
+
"stylelint-config-standard",
|
|
191
|
+
"stylelint-prettier/recommended"
|
|
192
|
+
],
|
|
193
|
+
"plugins": [
|
|
194
|
+
"stylelint-csstree-validator"
|
|
195
|
+
],
|
|
196
|
+
"rules": {
|
|
197
|
+
"csstree/validator": true,
|
|
198
|
+
"property-no-vendor-prefix": null,
|
|
199
|
+
"selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
|
|
200
|
+
"selector-no-vendor-prefix": null,
|
|
201
|
+
"value-no-vendor-prefix": null
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
JupyterLiteServer,
|
|
6
|
+
JupyterLiteServerPlugin,
|
|
7
|
+
Router
|
|
8
|
+
} from '@jupyterlite/server';
|
|
9
|
+
|
|
10
|
+
import { ITerminals } from './tokens';
|
|
11
|
+
import { Terminals } from './terminals';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The terminals service plugin.
|
|
15
|
+
*/
|
|
16
|
+
const terminalsPlugin: JupyterLiteServerPlugin<ITerminals> = {
|
|
17
|
+
id: '@jupyterlite/terminal:plugin',
|
|
18
|
+
description: 'A terminal for JupyterLite',
|
|
19
|
+
autoStart: true,
|
|
20
|
+
provides: ITerminals,
|
|
21
|
+
activate: async (app: JupyterLiteServer) => {
|
|
22
|
+
console.log(
|
|
23
|
+
'JupyterLab extension @jupyterlite/terminal:plugin is activated!'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { serviceManager } = app;
|
|
27
|
+
const { contents, serverSettings, terminals } = serviceManager;
|
|
28
|
+
console.log('terminals available:', terminals.isAvailable());
|
|
29
|
+
console.log('terminals ready:', terminals.isReady); // Not ready
|
|
30
|
+
console.log('terminals active:', terminals.isActive);
|
|
31
|
+
|
|
32
|
+
// Not sure this is necessary?
|
|
33
|
+
await terminals.ready;
|
|
34
|
+
console.log('terminals ready after await:', terminals.isReady); // Ready
|
|
35
|
+
|
|
36
|
+
return new Terminals(serverSettings.wsUrl, contents);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A plugin providing the routes for the terminals service
|
|
42
|
+
*/
|
|
43
|
+
const terminalsRoutesPlugin: JupyterLiteServerPlugin<void> = {
|
|
44
|
+
id: '@jupyterlite/terminal:routes-plugin',
|
|
45
|
+
autoStart: true,
|
|
46
|
+
requires: [ITerminals],
|
|
47
|
+
activate: (app: JupyterLiteServer, terminals: ITerminals) => {
|
|
48
|
+
console.log(
|
|
49
|
+
'JupyterLab extension @jupyterlite/terminal:routes-plugin is activated!',
|
|
50
|
+
terminals
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// GET /api/terminals - List the running terminals
|
|
54
|
+
app.router.get('/api/terminals', async (req: Router.IRequest) => {
|
|
55
|
+
const res = terminals.list();
|
|
56
|
+
// Should return last_activity for each too,
|
|
57
|
+
return new Response(JSON.stringify(res));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// POST /api/terminals - Start a terminal
|
|
61
|
+
app.router.post('/api/terminals', async (req: Router.IRequest) => {
|
|
62
|
+
const res = await terminals.startNew();
|
|
63
|
+
// Should return last_activity too.
|
|
64
|
+
return new Response(JSON.stringify(res));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default [terminalsPlugin, terminalsRoutesPlugin];
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { JupyterFileSystem, Shell, IFileSystem } from '@jupyterlite/cockle';
|
|
5
|
+
|
|
6
|
+
import { JSONPrimitive } from '@lumino/coreutils';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Server as WebSocketServer,
|
|
10
|
+
Client as WebSocketClient
|
|
11
|
+
} from 'mock-socket';
|
|
12
|
+
|
|
13
|
+
import { ITerminal } from './tokens';
|
|
14
|
+
|
|
15
|
+
export class Terminal implements ITerminal {
|
|
16
|
+
/**
|
|
17
|
+
* Construct a new Terminal.
|
|
18
|
+
*/
|
|
19
|
+
constructor(options: ITerminal.IOptions) {
|
|
20
|
+
this._name = options.name;
|
|
21
|
+
this._fs = new JupyterFileSystem(options.contentsManager);
|
|
22
|
+
console.log('==> new Terminal', this._name, this._fs);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the name of the terminal.
|
|
27
|
+
*/
|
|
28
|
+
get name(): string {
|
|
29
|
+
return this._name;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async wsConnect(url: string) {
|
|
33
|
+
console.log('==> Terminal.wsConnect', url);
|
|
34
|
+
|
|
35
|
+
// const server = new WebSocketServer(url, { mock: false });
|
|
36
|
+
const server = new WebSocketServer(url);
|
|
37
|
+
|
|
38
|
+
server.on('connection', async (socket: WebSocketClient) => {
|
|
39
|
+
console.log('==> server connection', this, socket);
|
|
40
|
+
|
|
41
|
+
const outputCallback = async (output: string) => {
|
|
42
|
+
console.log('==> recv from shell:', output);
|
|
43
|
+
const ret = JSON.stringify(['stdout', output]);
|
|
44
|
+
socket.send(ret);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
this._shell = new Shell(this._fs, outputCallback);
|
|
48
|
+
console.log('==> shell', this._shell);
|
|
49
|
+
|
|
50
|
+
socket.on('message', async (message: any) => {
|
|
51
|
+
const data = JSON.parse(message) as JSONPrimitive[];
|
|
52
|
+
console.log('==> socket message', data);
|
|
53
|
+
const message_type = data[0];
|
|
54
|
+
const content = data.slice(1);
|
|
55
|
+
|
|
56
|
+
if (message_type === 'stdin') {
|
|
57
|
+
await this._shell!.input(content[0] as string);
|
|
58
|
+
} else if (message_type === 'set_size') {
|
|
59
|
+
const rows = content[0] as number;
|
|
60
|
+
const columns = content[1] as number;
|
|
61
|
+
await this._shell!.setSize(rows, columns);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
socket.on('close', async () => {
|
|
66
|
+
console.log('==> socket close');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
socket.on('error', async () => {
|
|
70
|
+
console.log('==> socket error');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Return handshake.
|
|
74
|
+
const res = JSON.stringify(['setup']);
|
|
75
|
+
console.log('==> Returning handshake via socket', res);
|
|
76
|
+
socket.send(res);
|
|
77
|
+
|
|
78
|
+
await this._shell!.start();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private _name: string;
|
|
83
|
+
private _fs: IFileSystem;
|
|
84
|
+
private _shell?: Shell;
|
|
85
|
+
}
|
package/src/terminals.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { Contents, TerminalAPI } from '@jupyterlab/services';
|
|
5
|
+
|
|
6
|
+
import { Terminal } from './terminal';
|
|
7
|
+
import { ITerminals } from './tokens';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A class to handle requests to /api/terminals
|
|
11
|
+
*/
|
|
12
|
+
export class Terminals implements ITerminals {
|
|
13
|
+
/**
|
|
14
|
+
* Construct a new Terminals object.
|
|
15
|
+
*/
|
|
16
|
+
constructor(wsUrl: string, contentsManager: Contents.IManager) {
|
|
17
|
+
this._wsUrl = wsUrl;
|
|
18
|
+
this._contentsManager = contentsManager;
|
|
19
|
+
console.log(
|
|
20
|
+
'==> Terminals.constructor',
|
|
21
|
+
this._wsUrl,
|
|
22
|
+
this._contentsManager
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* List the running terminals.
|
|
28
|
+
*/
|
|
29
|
+
async list(): Promise<TerminalAPI.IModel[]> {
|
|
30
|
+
const ret = [...this._terminals.values()].map(terminal => ({
|
|
31
|
+
name: terminal.name
|
|
32
|
+
}));
|
|
33
|
+
console.log('==> Terminals.list', ret);
|
|
34
|
+
return ret;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start a new kernel.
|
|
39
|
+
*/
|
|
40
|
+
async startNew(): Promise<TerminalAPI.IModel> {
|
|
41
|
+
const name = this._nextAvailableName();
|
|
42
|
+
console.log('==> Terminals.new', name);
|
|
43
|
+
const term = new Terminal({ name, contentsManager: this._contentsManager });
|
|
44
|
+
this._terminals.set(name, term);
|
|
45
|
+
|
|
46
|
+
const url = `${this._wsUrl}terminals/websocket/${name}`;
|
|
47
|
+
await term.wsConnect(url);
|
|
48
|
+
|
|
49
|
+
return { name };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private _nextAvailableName(): string {
|
|
53
|
+
for (let i = 1; ; ++i) {
|
|
54
|
+
const name = `${i}`;
|
|
55
|
+
if (!this._terminals.has(name)) {
|
|
56
|
+
return name;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private _wsUrl: string;
|
|
62
|
+
private _contentsManager: Contents.IManager;
|
|
63
|
+
private _terminals: Map<string, Terminal> = new Map();
|
|
64
|
+
}
|
package/src/tokens.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright (c) Jupyter Development Team.
|
|
2
|
+
// Distributed under the terms of the Modified BSD License.
|
|
3
|
+
|
|
4
|
+
import { Contents, TerminalAPI } from '@jupyterlab/services';
|
|
5
|
+
|
|
6
|
+
import { Token } from '@lumino/coreutils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The token for the Terminals service.
|
|
10
|
+
*/
|
|
11
|
+
export const ITerminals = new Token<ITerminals>(
|
|
12
|
+
'@jupyterlite/terminal:ITerminals'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* An interface for the Terminals service.
|
|
17
|
+
*/
|
|
18
|
+
export interface ITerminals {
|
|
19
|
+
/**
|
|
20
|
+
* List the running terminals.
|
|
21
|
+
*/
|
|
22
|
+
list: () => Promise<TerminalAPI.IModel[]>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start a new kernel.
|
|
26
|
+
*/
|
|
27
|
+
startNew: () => Promise<TerminalAPI.IModel>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* An interface for a server-side terminal running in the browser.
|
|
32
|
+
*/
|
|
33
|
+
export interface ITerminal {
|
|
34
|
+
/**
|
|
35
|
+
* The name of the server-side terminal.
|
|
36
|
+
*/
|
|
37
|
+
readonly name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A namespace for ITerminal statics.
|
|
42
|
+
*/
|
|
43
|
+
export namespace ITerminal {
|
|
44
|
+
/**
|
|
45
|
+
* The instantiation options for an ITerminal.
|
|
46
|
+
*/
|
|
47
|
+
export interface IOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The name of the terminal.
|
|
50
|
+
*/
|
|
51
|
+
name: string;
|
|
52
|
+
|
|
53
|
+
contentsManager: Contents.IManager;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/style/base.css
ADDED
package/style/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import url('base.css');
|
package/style/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './base.css';
|