@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 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
+ [![Github Actions Status](https://github.com/jupyterlite/terminal/workflows/Build/badge.svg)](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
+ ![a screenshot showing a terminal running in JupyterLite](https://github.com/jupyterlite/terminal/assets/591645/1b4ff620-e8f2-4abf-b608-6badd66370ac)
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
@@ -0,0 +1,4 @@
1
+ import { JupyterLiteServerPlugin } from '@jupyterlite/server';
2
+ import { ITerminals } from './tokens';
3
+ declare const _default: (JupyterLiteServerPlugin<ITerminals> | JupyterLiteServerPlugin<void>)[];
4
+ export default _default;
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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3
+ */
4
+
5
+ describe('jupyterlite-terminal', () => {
6
+ it('should be tested', () => {
7
+ expect(1 + 1).toEqual(2);
8
+ });
9
+ });
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];
@@ -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
+ }
@@ -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
@@ -0,0 +1,5 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+
4
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5
+ */
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1 @@
1
+ import './base.css';