Browse Source

Backport PR #11637: Add `closeOnExit` terminal option (#11868)

Co-authored-by: David Brochart <david.brochart@gmail.com>
Frédéric Collonval 3 years ago
parent
commit
29f303ae0e

+ 8 - 6
packages/services/src/terminal/manager.ts

@@ -113,11 +113,9 @@ export class TerminalManager extends BaseManager implements Terminal.IManager {
   /*
    * Connect to a running terminal.
    *
-   * @param name - The name of the target terminal.
-   *
    * @param options - The options used to connect to the terminal.
    *
-   * @returns A promise that resolves to the new terminal connection instance.
+   * @returns The new terminal connection instance.
    *
    * #### Notes
    * The manager `serverSettings` will be used.
@@ -166,14 +164,18 @@ export class TerminalManager extends BaseManager implements Terminal.IManager {
   /**
    * Create a new terminal session.
    *
-   * @returns A promise that resolves with the terminal instance.
+   * @param options - The options used to create the terminal.
+   *
+   * @returns A promise that resolves with the terminal connection instance.
    *
    * #### Notes
    * The manager `serverSettings` will be used unless overridden in the
    * options.
    */
-  async startNew(): Promise<Terminal.ITerminalConnection> {
-    const model = await startNew(this.serverSettings);
+  async startNew(
+    options?: Terminal.ITerminal.IOptions
+  ): Promise<Terminal.ITerminalConnection> {
+    const model = await startNew(this.serverSettings, options?.name);
     await this.refreshRunning();
     return this.connectTo({ model });
   }

+ 10 - 4
packages/services/src/terminal/restapi.ts

@@ -30,16 +30,22 @@ export interface IModel {
 /**
  * Start a new terminal session.
  *
- * @param options - The session options to use.
+ * @param settings - The server settings to use.
+ *
+ * @param name - The name of the target terminal.
  *
- * @returns A promise that resolves with the session instance.
+ * @returns A promise that resolves with the session model.
  */
 export async function startNew(
-  settings: ServerConnection.ISettings = ServerConnection.makeSettings()
+  settings: ServerConnection.ISettings = ServerConnection.makeSettings(),
+  name?: string
 ): Promise<IModel> {
   Private.errorIfNotAvailable();
   const url = URLExt.join(settings.baseUrl, TERMINAL_SERVICE_URL);
-  const init = { method: 'POST' };
+  const init = {
+    method: 'POST',
+    body: JSON.stringify({ name })
+  };
 
   const response = await ServerConnection.makeRequest(url, init, settings);
   if (response.status !== 200) {

+ 14 - 7
packages/services/src/terminal/terminal.ts

@@ -16,6 +16,15 @@ import { IManager as IBaseManager } from '../basemanager';
 import { IModel, isAvailable } from './restapi';
 export { IModel, isAvailable };
 
+export namespace ITerminal {
+  export interface IOptions {
+    /**
+     * Terminal name.
+     */
+    name: string;
+  }
+}
+
 /**
  * An interface for a terminal session.
  */
@@ -145,23 +154,21 @@ export interface IManager extends IBaseManager {
   /**
    * Create a new terminal session.
    *
-   * @param options - The options used to create the session.
+   * @param options - The options used to create the terminal.
    *
-   * @returns A promise that resolves with the terminal instance.
+   * @returns A promise that resolves with the terminal connection instance.
    *
    * #### Notes
    * The manager `serverSettings` will be always be used.
    */
-  startNew(
-    options?: ITerminalConnection.IOptions
-  ): Promise<ITerminalConnection>;
+  startNew(options?: ITerminal.IOptions): Promise<ITerminalConnection>;
 
   /*
    * Connect to a running session.
    *
-   * @param name - The name of the target session.
+   * @param options - The options used to connect to the terminal.
    *
-   * @returns A promise that resolves with the new session instance.
+   * @returns The new terminal connection instance.
    */
   connectTo(
     options: Omit<ITerminalConnection.IOptions, 'serverSettings'>

+ 6 - 0
packages/terminal-extension/schema/plugin.json

@@ -81,6 +81,12 @@
       "type": "boolean",
       "default": false
     },
+    "closeOnExit": {
+      "title": "Close on exit",
+      "description": "Close the widget when exiting the terminal.",
+      "type": "boolean",
+      "default": true
+    },
     "pasteWithCtrlV": {
       "title": "Paste with Ctrl+V",
       "description": "Enable pasting with Ctrl+V.  This can be disabled to use Ctrl+V in the vi editor, for instance.  This setting has no effect on macOS, where Cmd+V is available",

+ 18 - 4
packages/terminal-extension/src/index.ts

@@ -19,7 +19,7 @@ import {
 import { ILauncher } from '@jupyterlab/launcher';
 import { IFileMenu, IMainMenu } from '@jupyterlab/mainmenu';
 import { IRunningSessionManagers, IRunningSessions } from '@jupyterlab/running';
-import { Terminal } from '@jupyterlab/services';
+import { Terminal, TerminalAPI } from '@jupyterlab/services';
 import { ISettingRegistry } from '@jupyterlab/settingregistry';
 import { ITerminal, ITerminalTracker } from '@jupyterlab/terminal';
 // Name-only import so as to not trigger inclusion in main bundle
@@ -348,9 +348,23 @@ export function addCommands(
 
       const name = args['name'] as string;
 
-      const session = await (name
-        ? serviceManager.terminals.connectTo({ model: { name } })
-        : serviceManager.terminals.startNew());
+      let session;
+      if (name) {
+        const models = await TerminalAPI.listRunning();
+        if (models.map(d => d.name).includes(name)) {
+          // we are restoring a terminal widget and the corresponding terminal exists
+          // let's connect to it
+          session = serviceManager.terminals.connectTo({ model: { name } });
+        } else {
+          // we are restoring a terminal widget but the corresponding terminal was closed
+          // let's start a new terminal with the original name
+          session = await serviceManager.terminals.startNew({ name });
+        }
+      } else {
+        // we are creating a new terminal widget with a new terminal
+        // let the server choose the terminal name
+        session = await serviceManager.terminals.startNew();
+      }
 
       const term = new Terminal(session, options, translator);
 

+ 6 - 0
packages/terminal/src/tokens.ts

@@ -82,6 +82,11 @@ export namespace ITerminal {
      */
     shutdownOnClose: boolean;
 
+    /**
+     * Whether to close the widget when exiting a terminal or not.
+     */
+    closeOnExit: boolean;
+
     /**
      * Whether to blink the cursor.  Can only be set at startup.
      */
@@ -125,6 +130,7 @@ export namespace ITerminal {
     lineHeight: 1.0,
     scrollback: 1000,
     shutdownOnClose: false,
+    closeOnExit: true,
     cursorBlink: true,
     initialCommand: '',
     screenReaderMode: false, // False by default, can cause scrollbar mouse interaction issues.

+ 6 - 1
packages/terminal/src/widget.ts

@@ -71,7 +71,11 @@ export class Terminal extends Widget implements ITerminal.ITerminal {
     this.title.label = this._trans.__('Terminal');
 
     session.messageReceived.connect(this._onMessage, this);
-    session.disposed.connect(this.dispose, this);
+    session.disposed.connect(() => {
+      if (this.getOption('closeOnExit')) {
+        this.dispose();
+      }
+    }, this);
 
     if (session.connectionStatus === 'connected') {
       this._initialConnection();
@@ -148,6 +152,7 @@ export class Terminal extends Widget implements ITerminal.ITerminal {
 
     switch (option) {
       case 'shutdownOnClose': // Do not transmit to XTerm
+      case 'closeOnExit': // Do not transmit to XTerm
         break;
       case 'theme':
         this._term.setOption(