diff --git a/builder/index.js b/builder/index.js
index 0bfa6af9c..b1e968cff 100644
--- a/builder/index.js
+++ b/builder/index.js
@@ -72,6 +72,8 @@ async function main() {
require('@jupyterlab-classic/application-extension'),
require('@jupyterlab-classic/docmanager-extension'),
require('@jupyterlab-classic/notebook-extension'),
+ // to handle opening new tabs after creating a new terminal
+ require('@jupyterlab-classic/terminal-extension'),
// @jupyterlab plugins
require('@jupyterlab/apputils-extension').default.filter(({ id }) =>
@@ -99,6 +101,8 @@ async function main() {
),
require('@jupyterlab/rendermime-extension'),
require('@jupyterlab/shortcuts-extension'),
+ // so new terminals can be create from the menu
+ require('@jupyterlab/terminal-extension'),
require('@jupyterlab/theme-light-extension'),
require('@jupyterlab/theme-dark-extension')
];
diff --git a/builder/package.json b/builder/package.json
index a06fd39f4..1192f6792 100644
--- a/builder/package.json
+++ b/builder/package.json
@@ -14,6 +14,7 @@
"@jupyterlab-classic/application-extension": "^0.1.0",
"@jupyterlab-classic/docmanager-extension": "^0.1.0",
"@jupyterlab-classic/notebook-extension": "^0.1.0",
+ "@jupyterlab-classic/terminal-extension": "^0.1.0",
"@jupyterlab-classic/tree-extension": "^0.1.0",
"@jupyterlab-classic/ui-components": "^0.1.0",
"@jupyterlab/apputils-extension": "^3.0.0-rc.12",
@@ -27,6 +28,7 @@
"@jupyterlab/rendermime-extension": "^3.0.0-rc.12",
"@jupyterlab/running-extension": "^3.0.0-rc.12",
"@jupyterlab/shortcuts-extension": "^3.0.0-rc.12",
+ "@jupyterlab/terminal-extension": "^3.0.0-rc.12",
"@jupyterlab/tooltip-extension": "^3.0.0-rc.12",
"@jupyterlab/theme-light-extension": "^3.0.0-rc.12",
"@jupyterlab/theme-dark-extension": "^3.0.0-rc.12",
diff --git a/builder/style.css b/builder/style.css
index 4d98db16d..8e96fce99 100644
--- a/builder/style.css
+++ b/builder/style.css
@@ -8,6 +8,7 @@
@import url('~@jupyterlab/fileeditor/style/index.css');
@import url('~@jupyterlab/tooltip/style/index.css');
@import url('~@jupyterlab/running/style/index.css');
+@import url('~@jupyterlab/terminal/style/index.css');
@import url('~@jupyterlab/codemirror-extension/style/index.css');
@import url('~@jupyterlab/docmanager-extension/style/index.css');
diff --git a/jupyterlab_classic/app.py b/jupyterlab_classic/app.py
index d8e06f0cd..06ba38530 100644
--- a/jupyterlab_classic/app.py
+++ b/jupyterlab_classic/app.py
@@ -77,6 +77,7 @@ class ClassicTreeHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, Ext
@web.authenticated
def get(self, path=None):
page_config = self.get_page_config()
+ page_config['terminalsAvailable'] = self.settings['terminals_available']
return self.write(
self.render_template(
"tree.html",
@@ -87,6 +88,21 @@ class ClassicTreeHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, Ext
)
+class ClassicTerminalHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler):
+ @web.authenticated
+ def get(self, path=None):
+ page_config = self.get_page_config()
+ page_config['terminalsAvailable'] = self.settings['terminals_available']
+ return self.write(
+ self.render_template(
+ "terminals.html",
+ base_url=self.base_url,
+ token=self.settings["token"],
+ page_config=page_config,
+ )
+ )
+
+
class ClassicFileHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler):
@web.authenticated
def get(self, path=None):
@@ -100,6 +116,7 @@ class ClassicFileHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, Ext
)
)
+
class ClassicNotebookHandler(ClassicPageConfigMixin, ExtensionHandlerJinjaMixin, ExtensionHandlerMixin, JupyterHandler):
@web.authenticated
def get(self, path=None):
@@ -132,6 +149,7 @@ class ClassicApp(NBClassicConfigShimMixin, LabServerApp):
self.handlers.append(("/classic/tree(.*)", ClassicTreeHandler))
self.handlers.append(("/classic/notebooks(.*)", ClassicNotebookHandler))
self.handlers.append(("/classic/edit(.*)", ClassicFileHandler))
+ self.handlers.append(("/classic/terminals/(.*)", ClassicTerminalHandler))
def initialize_templates(self):
super().initialize_templates()
diff --git a/jupyterlab_classic/templates/terminals.html b/jupyterlab_classic/templates/terminals.html
new file mode 100644
index 000000000..c1d0ed66d
--- /dev/null
+++ b/jupyterlab_classic/templates/terminals.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ {{page_config['appName'] | e}} - Terminal
+
+
+
+ {# Copy so we do not modify the page_config with updates. #}
+ {% set page_config_full = page_config.copy() %}
+
+ {# Set a dummy variable - we just want the side effect of the update. #}
+ {% set _ = page_config_full.update(baseUrl=base_url, wsUrl=ws_url) %}
+
+ {# Sentinel value to say that we are on the tree page #}
+ {% set _ = page_config_full.update(classicPage='terminals') %}
+
+
+
+
+
+
+
+
diff --git a/packages/application-extension/style/base.css b/packages/application-extension/style/base.css
index 33b4bd84f..1c814d5cd 100644
--- a/packages/application-extension/style/base.css
+++ b/packages/application-extension/style/base.css
@@ -13,3 +13,7 @@
.jp-Document {
height: 100%;
}
+
+.jp-MainAreaWidget {
+ height: 100%;
+}
diff --git a/packages/application/src/shell.ts b/packages/application/src/shell.ts
index 24ded8f43..86bb6216f 100644
--- a/packages/application/src/shell.ts
+++ b/packages/application/src/shell.ts
@@ -102,11 +102,9 @@ export class ClassicShell extends Widget implements JupyterFrontEnd.IShell {
if (area === 'menu') {
return this._menuHandler.addWidget(widget, rank);
}
- if (area === 'main') {
+ if (area === 'main' || area === undefined) {
if (this._main.widgets.length > 0) {
// do not add the widget if there is already one
- // TODO: should the widget be closed?
- widget.close();
return;
}
this._main.addWidget(widget);
diff --git a/packages/terminal-extension/package.json b/packages/terminal-extension/package.json
new file mode 100644
index 000000000..344b33563
--- /dev/null
+++ b/packages/terminal-extension/package.json
@@ -0,0 +1,54 @@
+{
+ "name": "@jupyterlab-classic/terminal-extension",
+ "version": "0.1.0",
+ "description": "JupyterLab Classic - Terminal Extension",
+ "homepage": "https://github.com/jtpio/jupyterlab-classic",
+ "bugs": {
+ "url": "https://github.com/jtpio/jupyterlab-classic/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jtpio/jupyterlab-classic.git"
+ },
+ "license": "BSD-3-Clause",
+ "author": "Project Jupyter",
+ "sideEffects": [
+ "style/**/*.css"
+ ],
+ "main": "lib/index.js",
+ "types": "lib/index.d.ts",
+ "style": "style/index.css",
+ "directories": {
+ "lib": "lib/"
+ },
+ "files": [
+ "lib/*.d.ts",
+ "lib/*.js.map",
+ "lib/*.js",
+ "schema/*.json",
+ "style/**/*.css"
+ ],
+ "scripts": {
+ "build": "tsc -b",
+ "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
+ "docs": "typedoc src",
+ "prepublishOnly": "npm run build",
+ "watch": "tsc -b --watch"
+ },
+ "dependencies": {
+ "@jupyterlab/application": "^3.0.0-rc.12",
+ "@jupyterlab/coreutils": "^5.0.0-rc.12",
+ "@jupyterlab/terminal": "^3.0.0-rc.12",
+ "@lumino/algorithm": "^1.3.3"
+ },
+ "devDependencies": {
+ "rimraf": "~3.0.0",
+ "typescript": "~4.0.2"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "jupyterlab": {
+ "extension": true
+ }
+}
diff --git a/packages/terminal-extension/src/index.ts b/packages/terminal-extension/src/index.ts
new file mode 100644
index 000000000..06ef925e2
--- /dev/null
+++ b/packages/terminal-extension/src/index.ts
@@ -0,0 +1,77 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+ IRouter,
+ JupyterFrontEnd,
+ JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import { PageConfig } from '@jupyterlab/coreutils';
+
+import { ITerminalTracker } from '@jupyterlab/terminal';
+
+import { find } from '@lumino/algorithm';
+
+/**
+ * A plugin to terminals in a new tab
+ */
+const opener: JupyterFrontEndPlugin = {
+ id: '@jupyterlab-classic/terminal-extension:opener',
+ requires: [IRouter],
+ autoStart: true,
+ activate: (app: JupyterFrontEnd, router: IRouter) => {
+ const { commands } = app;
+ const terminalPattern = new RegExp('/terminals/(.*)');
+
+ const command = 'router:terminal';
+ commands.addCommand(command, {
+ execute: (args: any) => {
+ const parsed = args as IRouter.ILocation;
+ const matches = parsed.path.match(terminalPattern);
+ if (!matches) {
+ return;
+ }
+ const [, name] = matches;
+ if (!name) {
+ return;
+ }
+
+ commands.execute('terminal:open', { name });
+ }
+ });
+
+ router.register({ command, pattern: terminalPattern });
+ }
+};
+
+/**
+ * Open terminals in a new tab.
+ */
+const redirect: JupyterFrontEndPlugin = {
+ id: '@jupyterlab-classic/terminal-extension:redirect',
+ requires: [ITerminalTracker],
+ autoStart: true,
+ activate: (app: JupyterFrontEnd, tracker: ITerminalTracker) => {
+ const baseUrl = PageConfig.getBaseUrl();
+ tracker.widgetAdded.connect((send, terminal) => {
+ const widget = find(app.shell.widgets('main'), w => w.id === terminal.id);
+ if (widget) {
+ // bail if the terminal is already added to the main area
+ return;
+ }
+ const name = terminal.content.session.name;
+ window.open(`${baseUrl}classic/terminals/${name}`, '_blank');
+
+ // dispose the widget since it is not used
+ terminal.dispose();
+ });
+ }
+};
+
+/**
+ * Export the plugins as default.
+ */
+const plugins: JupyterFrontEndPlugin[] = [opener, redirect];
+
+export default plugins;
diff --git a/packages/terminal-extension/style/base.css b/packages/terminal-extension/style/base.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/terminal-extension/style/index.css b/packages/terminal-extension/style/index.css
new file mode 100644
index 000000000..f5246e666
--- /dev/null
+++ b/packages/terminal-extension/style/index.css
@@ -0,0 +1 @@
+@import url('./base.css');
diff --git a/packages/terminal-extension/tsconfig.json b/packages/terminal-extension/tsconfig.json
new file mode 100644
index 000000000..399b75b7a
--- /dev/null
+++ b/packages/terminal-extension/tsconfig.json
@@ -0,0 +1,8 @@
+{
+ "extends": "../../tsconfigbase",
+ "compilerOptions": {
+ "outDir": "lib",
+ "rootDir": "src"
+ },
+ "include": ["src/**/*"]
+}
diff --git a/yarn.lock b/yarn.lock
index 4244c057e..4fecc1a69 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2339,6 +2339,39 @@
react "^17.0.1"
typestyle "^2.0.4"
+"@jupyterlab/terminal-extension@^3.0.0-rc.12":
+ version "3.0.0-rc.13"
+ resolved "https://registry.yarnpkg.com/@jupyterlab/terminal-extension/-/terminal-extension-3.0.0-rc.13.tgz#e457ddafe23495488852cf0e26e6fbbebbefd677"
+ integrity sha512-nVmcZzRiD9te3lR8icnJ30HAjXtDXLJwqFS/GhpVV7CUCOxNjyQQirE5Nn4aNGMtp8/7g6syRbKYPpSyeNut7A==
+ dependencies:
+ "@jupyterlab/application" "^3.0.0-rc.13"
+ "@jupyterlab/apputils" "^3.0.0-rc.13"
+ "@jupyterlab/launcher" "^3.0.0-rc.13"
+ "@jupyterlab/mainmenu" "^3.0.0-rc.13"
+ "@jupyterlab/running" "^3.0.0-rc.13"
+ "@jupyterlab/services" "^6.0.0-rc.13"
+ "@jupyterlab/settingregistry" "^3.0.0-rc.13"
+ "@jupyterlab/terminal" "^3.0.0-rc.13"
+ "@jupyterlab/translation" "^3.0.0-rc.13"
+ "@jupyterlab/ui-components" "^3.0.0-rc.13"
+ "@lumino/algorithm" "^1.3.3"
+ "@lumino/widgets" "^1.16.1"
+
+"@jupyterlab/terminal@^3.0.0-rc.12", "@jupyterlab/terminal@^3.0.0-rc.13":
+ version "3.0.0-rc.13"
+ resolved "https://registry.yarnpkg.com/@jupyterlab/terminal/-/terminal-3.0.0-rc.13.tgz#3651a03b96691bae22a0562f6a6a22f2f718ff25"
+ integrity sha512-n1Wl9HAeGvkpuHJIJpd5JUsrTJtrbzDP7XAdWn+SuITdTPxz5cleT5lAxk5jRukN69r9S9nicLig+WHJgQL8AA==
+ dependencies:
+ "@jupyterlab/apputils" "^3.0.0-rc.13"
+ "@jupyterlab/services" "^6.0.0-rc.13"
+ "@jupyterlab/translation" "^3.0.0-rc.13"
+ "@lumino/coreutils" "^1.5.3"
+ "@lumino/domutils" "^1.2.3"
+ "@lumino/messaging" "^1.4.3"
+ "@lumino/widgets" "^1.16.1"
+ xterm "~4.8.1"
+ xterm-addon-fit "~0.4.0"
+
"@jupyterlab/testutils@^3.0.0-rc.12":
version "3.0.0-rc.12"
resolved "https://registry.yarnpkg.com/@jupyterlab/testutils/-/testutils-3.0.0-rc.12.tgz#accdebf76e700552328913ab2a7261031844776d"
@@ -12406,6 +12439,16 @@ xtend@^4.0.1, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+xterm-addon-fit@~0.4.0:
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.4.0.tgz#06e0c5d0a6aaacfb009ef565efa1c81e93d90193"
+ integrity sha512-p4BESuV/g2L6pZzFHpeNLLnep9mp/DkF3qrPglMiucSFtD8iJxtMufEoEJbN8LZwB4i+8PFpFvVuFrGOSpW05w==
+
+xterm@~4.8.1:
+ version "4.8.1"
+ resolved "https://registry.yarnpkg.com/xterm/-/xterm-4.8.1.tgz#155a1729a43e1a89b406524e22c5634339e39ca1"
+ integrity sha512-ax91ny4tI5eklqIfH79OUSGE2PUX2rGbwONmB6DfqpyhSZO8/cf++sqiaMWEVCMjACyMfnISW7C3gGMoNvNolQ==
+
y18n@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"