diff --git a/configs/root-compilation.tsconfig.json b/configs/root-compilation.tsconfig.json
index a2524c2381a6b..6ca6c16407c40 100644
--- a/configs/root-compilation.tsconfig.json
+++ b/configs/root-compilation.tsconfig.json
@@ -37,6 +37,9 @@
{
"path": "../packages/editor/compile.tsconfig.json"
},
+ {
+ "path": "../packages/external-terminal/compile.tsconfig.json"
+ },
{
"path": "../packages/file-search/compile.tsconfig.json"
},
diff --git a/examples/electron/compile.tsconfig.json b/examples/electron/compile.tsconfig.json
index a121e3fa9b92f..82d0e06db7287 100644
--- a/examples/electron/compile.tsconfig.json
+++ b/examples/electron/compile.tsconfig.json
@@ -35,6 +35,9 @@
{
"path": "../../packages/editor-preview/compile.tsconfig.json"
},
+ {
+ "path": "../../packages/external-terminal/compile.tsconfig.json"
+ },
{
"path": "../../packages/file-search/compile.tsconfig.json"
},
diff --git a/examples/electron/package.json b/examples/electron/package.json
index ccf6f307f1d65..58a3adf2c98c2 100644
--- a/examples/electron/package.json
+++ b/examples/electron/package.json
@@ -23,6 +23,7 @@
"@theia/editor": "1.11.0",
"@theia/editor-preview": "1.11.0",
"@theia/electron": "1.11.0",
+ "@theia/external-terminal": "1.11.0",
"@theia/file-search": "1.11.0",
"@theia/filesystem": "1.11.0",
"@theia/getting-started": "1.11.0",
diff --git a/packages/external-terminal/.eslintrc.js b/packages/external-terminal/.eslintrc.js
new file mode 100644
index 0000000000000..be9cf1a1b3dff
--- /dev/null
+++ b/packages/external-terminal/.eslintrc.js
@@ -0,0 +1,10 @@
+/** @type {import('eslint').Linter.Config} */
+module.exports = {
+ extends: [
+ '../../configs/build.eslintrc.json'
+ ],
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: 'compile.tsconfig.json'
+ }
+};
diff --git a/packages/external-terminal/README.md b/packages/external-terminal/README.md
new file mode 100644
index 0000000000000..bd6a4f7387057
--- /dev/null
+++ b/packages/external-terminal/README.md
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
ECLIPSE THEIA - EXTERNAL-TERMINAL EXTENSION
+
+
+
+
+
+## Description
+
+The `@theia/external-terminal` extension contributes the ability to spawn external terminals for `electron` applications.
+The extension includes the necessary logic to spawn the appropriate terminal application for each operating system (Windows, Linux, OSX)
+by identifying certain environment variables. The extension also contributes preferences to control this behavior if necessary.
+
+**Note:** The extension does not support browser applications.
+
+## Contributions
+
+### Commands
+
+- `OPEN_NATIVE_CONSOLE`: spawns an external terminal (native console) for different use-cases.
+
+### Preferences
+
+- `terminal.external.windowsExec`: the application executable for Windows.
+- `terminal.external.linuxExec`: the application executable for Linux.
+- `terminal.external.osxExec`: the application executable for OSX.
+
+## Additional Information
+
+- [Theia - GitHub](https://github.com/eclipse-theia/theia)
+- [Theia - Website](https://theia-ide.org/)
+
+## License
+
+- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
+- [δΈ€ (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
+
+## Trademark
+
+"Theia" is a trademark of the Eclipse Foundation
+https://www.eclipse.org/theia
diff --git a/packages/external-terminal/compile.tsconfig.json b/packages/external-terminal/compile.tsconfig.json
new file mode 100644
index 0000000000000..7095e7cbbee90
--- /dev/null
+++ b/packages/external-terminal/compile.tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "extends": "../../configs/base.tsconfig",
+ "compilerOptions": {
+ "composite": true,
+ "rootDir": "src",
+ "outDir": "lib"
+ },
+ "include": [
+ "src"
+ ],
+ "references": [
+ {
+ "path": "../core/compile.tsconfig.json"
+ },
+ {
+ "path": "../editor/compile.tsconfig.json"
+ },
+ {
+ "path": "../workspace/compile.tsconfig.json"
+ }
+ ]
+}
diff --git a/packages/external-terminal/package.json b/packages/external-terminal/package.json
new file mode 100644
index 0000000000000..2898c767484a0
--- /dev/null
+++ b/packages/external-terminal/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@theia/external-terminal",
+ "version": "1.11.0",
+ "description": "Theia - External Terminal Extension",
+ "dependencies": {
+ "@theia/core": "1.11.0",
+ "@theia/editor": "1.11.0",
+ "@theia/workspace": "1.11.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "theiaExtensions": [
+ {
+ "backendElectron": "lib/electron-node/external-terminal-backend-module",
+ "frontendElectron": "lib/electron-browser/external-terminal-frontend-module"
+ }
+ ],
+ "keywords": [
+ "theia-extension"
+ ],
+ "license": "EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/eclipse-theia/theia.git"
+ },
+ "bugs": {
+ "url": "https://github.com/eclipse-theia/theia/issues"
+ },
+ "homepage": "https://github.com/eclipse-theia/theia",
+ "files": [
+ "lib",
+ "src"
+ ],
+ "scripts": {
+ "lint": "theiaext lint",
+ "build": "theiaext build",
+ "watch": "theiaext watch",
+ "clean": "theiaext clean",
+ "test": "theiaext test"
+ },
+ "devDependencies": {
+ "@theia/ext-scripts": "^1.9.0"
+ },
+ "nyc": {
+ "extends": "../../configs/nyc.json"
+ }
+}
diff --git a/packages/external-terminal/src/common/external-terminal.ts b/packages/external-terminal/src/common/external-terminal.ts
new file mode 100644
index 0000000000000..aa73aba2d7283
--- /dev/null
+++ b/packages/external-terminal/src/common/external-terminal.ts
@@ -0,0 +1,55 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+export const ExternalTerminalService = Symbol('ExternalTerminalService');
+export const externalTerminalServicePath = '/services/external-terminal';
+
+/**
+ * Represents the external terminal configuration options.
+ */
+export interface ExternalTerminalConfiguration {
+ /**
+ * The external terminal executable for Windows.
+ */
+ 'terminal.external.windowsExec': string;
+ /**
+ * The external terminal executable for OSX.
+ */
+ 'terminal.external.osxExec': string;
+ /**
+ * The external terminal executable for Linux.
+ */
+ 'terminal.external.linuxExec': string;
+}
+
+export interface ExternalTerminalService {
+
+ /**
+ * Open a native terminal in the designated working directory.
+ *
+ * @param configuration the configuration for opening external terminals.
+ * @param cwd the string URI of the current working directory where the terminal should open from.
+ */
+ openTerminal(configuration: ExternalTerminalConfiguration, cwd: string): Promise;
+
+ /**
+ * Get the default executable.
+ *
+ * @returns the default terminal executable.
+ */
+ getDefaultExec(): Promise;
+
+}
diff --git a/packages/external-terminal/src/electron-browser/external-terminal-contribution.ts b/packages/external-terminal/src/electron-browser/external-terminal-contribution.ts
new file mode 100644
index 0000000000000..714ab3b53a633
--- /dev/null
+++ b/packages/external-terminal/src/electron-browser/external-terminal-contribution.ts
@@ -0,0 +1,114 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { inject, injectable } from 'inversify';
+import { Command, CommandContribution, CommandRegistry } from '@theia/core/lib/common';
+import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
+import { QuickPickService } from '@theia/core/lib/common/quick-pick-service';
+import { KeybindingContribution, KeybindingRegistry, LabelProvider } from '@theia/core/lib/browser';
+import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
+import { WorkspaceService } from '@theia/workspace/lib/browser';
+import { ExternalTerminalService } from '../common/external-terminal';
+import { ExternalTerminalPreferenceService } from './external-terminal-preference';
+
+export namespace ExternalTerminalCommands {
+ export const OPEN_NATIVE_CONSOLE: Command = {
+ id: 'workbench.action.terminal.openNativeConsole',
+ label: 'Open New External Terminal'
+ };
+}
+
+@injectable()
+export class ExternalTerminalFrontendContribution implements CommandContribution, KeybindingContribution {
+
+ @inject(EditorManager)
+ protected readonly editorManager: EditorManager;
+
+ @inject(EnvVariablesServer)
+ protected readonly envVariablesServer: EnvVariablesServer;
+
+ @inject(LabelProvider)
+ protected readonly labelProvider: LabelProvider;
+
+ @inject(QuickPickService)
+ protected readonly quickPickService: QuickPickService;
+
+ @inject(ExternalTerminalService)
+ protected readonly externalTerminalService: ExternalTerminalService;
+
+ @inject(ExternalTerminalPreferenceService)
+ protected readonly externalTerminalPreferences: ExternalTerminalPreferenceService;
+
+ @inject(WorkspaceService)
+ protected readonly workspaceService: WorkspaceService;
+
+ registerCommands(commands: CommandRegistry): void {
+ commands.registerCommand(ExternalTerminalCommands.OPEN_NATIVE_CONSOLE, {
+ execute: () => this.openExternalTerminal()
+ });
+ }
+
+ registerKeybindings(keybindings: KeybindingRegistry): void {
+ keybindings.registerKeybinding({
+ command: ExternalTerminalCommands.OPEN_NATIVE_CONSOLE.id,
+ keybinding: 'ctrlcmd+shift+c',
+ when: '!terminalFocus'
+ });
+ }
+
+ /**
+ * Open a native console on the host machine.
+ *
+ * - If multi-root workspace is open, displays a quick pick to let users choose which workspace to spawn the terminal.
+ * - If only one workspace is open, the terminal spawns at the root of the current workspace.
+ * - If no workspace is open and there is an active editor, the terminal spawns at the parent folder of that file.
+ * - If no workspace is open and there are no active editors, the terminal spawns at user home directory.
+ */
+ protected async openExternalTerminal(): Promise {
+ const configuration = this.externalTerminalPreferences.getExternalTerminalConfiguration();
+
+ if (this.workspaceService.isMultiRootWorkspaceOpened) {
+ const chosenWorkspaceRoot = await this.selectCwd();
+ if (chosenWorkspaceRoot) {
+ await this.externalTerminalService.openTerminal(configuration, chosenWorkspaceRoot);
+ }
+ return;
+ }
+
+ if (this.workspaceService.opened) {
+ const workspaceRootUri = this.workspaceService.tryGetRoots()[0].resource;
+ await this.externalTerminalService.openTerminal(configuration, workspaceRootUri.toString());
+ return;
+ }
+
+ const fallbackUri = this.editorManager.activeEditor?.editor.uri.parent ?? await this.envVariablesServer.getHomeDirUri();
+ await this.externalTerminalService.openTerminal(configuration, fallbackUri.toString());
+ }
+
+ /**
+ * Display a quick pick for user to choose a target workspace in opened workspaces.
+ */
+ protected async selectCwd(): Promise {
+ const roots = this.workspaceService.tryGetRoots();
+ return this.quickPickService.show(roots.map(
+ ({ resource }) => ({
+ label: this.labelProvider.getName(resource),
+ description: this.labelProvider.getLongName(resource),
+ value: resource.toString()
+ })
+ ), { placeholder: 'Select current working directory for new external terminal' });
+ }
+}
diff --git a/packages/external-terminal/src/electron-browser/external-terminal-frontend-module.ts b/packages/external-terminal/src/electron-browser/external-terminal-frontend-module.ts
new file mode 100644
index 0000000000000..4daf76fbbe42c
--- /dev/null
+++ b/packages/external-terminal/src/electron-browser/external-terminal-frontend-module.ts
@@ -0,0 +1,33 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { ContainerModule, interfaces } from 'inversify';
+import { CommandContribution } from '@theia/core/lib/common';
+import { KeybindingContribution, WebSocketConnectionProvider } from '@theia/core/lib/browser';
+import { bindExternalTerminalPreferences } from './external-terminal-preference';
+import { ExternalTerminalFrontendContribution } from './external-terminal-contribution';
+import { ExternalTerminalService, externalTerminalServicePath } from '../common/external-terminal';
+
+export default new ContainerModule((bind: interfaces.Bind) => {
+ bind(ExternalTerminalFrontendContribution).toSelf().inSingletonScope();
+ bindExternalTerminalPreferences(bind);
+ [CommandContribution, KeybindingContribution].forEach(serviceIdentifier =>
+ bind(serviceIdentifier).toService(ExternalTerminalFrontendContribution)
+ );
+ bind(ExternalTerminalService).toDynamicValue(ctx =>
+ WebSocketConnectionProvider.createProxy(ctx.container, externalTerminalServicePath)
+ ).inSingletonScope();
+});
diff --git a/packages/external-terminal/src/electron-browser/external-terminal-preference.ts b/packages/external-terminal/src/electron-browser/external-terminal-preference.ts
new file mode 100644
index 0000000000000..5c5845502ea08
--- /dev/null
+++ b/packages/external-terminal/src/electron-browser/external-terminal-preference.ts
@@ -0,0 +1,104 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { inject, injectable, interfaces, postConstruct } from 'inversify';
+import {
+ createPreferenceProxy,
+ PreferenceSchema,
+ PreferenceService,
+ PreferenceProxy
+} from '@theia/core/lib/browser';
+import { PreferenceSchemaProvider } from '@theia/core/lib/browser/preferences/preference-contribution';
+import { isWindows, isOSX } from '@theia/core/lib/common/os';
+import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal';
+
+export const ExternalTerminalPreferences = Symbol('ExternalTerminalPreferences');
+export type ExternalTerminalPreferences = PreferenceProxy;
+
+export const ExternalTerminalSchemaPromise = Symbol('ExternalTerminalSchemaPromise');
+export type ExternalTerminalSchemaPromise = Promise;
+
+export function bindExternalTerminalPreferences(bind: interfaces.Bind): void {
+ bind(ExternalTerminalSchemaPromise).toDynamicValue(
+ ctx => getExternalTerminalSchema(ctx.container.get(ExternalTerminalService))
+ ).inSingletonScope();
+ bind(ExternalTerminalPreferences).toDynamicValue(
+ ctx => createPreferenceProxy(
+ ctx.container.get(PreferenceService),
+ ctx.container.get(ExternalTerminalSchemaPromise),
+ )
+ ).inSingletonScope();
+ bind(ExternalTerminalPreferenceService).toSelf().inSingletonScope();
+}
+
+@injectable()
+export class ExternalTerminalPreferenceService {
+
+ @inject(ExternalTerminalPreferences)
+ protected readonly preferences: ExternalTerminalPreferences;
+
+ @inject(PreferenceSchemaProvider)
+ protected readonly preferenceSchemaProvider: PreferenceSchemaProvider;
+
+ @inject(ExternalTerminalSchemaPromise)
+ protected readonly promisedSchema: ExternalTerminalSchemaPromise;
+
+ @postConstruct()
+ protected init(): void {
+ this.promisedSchema.then(schema => this.preferenceSchemaProvider.setSchema(schema));
+ }
+
+ /**
+ * Get the external terminal configurations from preferences.
+ */
+ getExternalTerminalConfiguration(): ExternalTerminalConfiguration {
+ return {
+ 'terminal.external.linuxExec': this.preferences['terminal.external.linuxExec'],
+ 'terminal.external.osxExec': this.preferences['terminal.external.osxExec'],
+ 'terminal.external.windowsExec': this.preferences['terminal.external.windowsExec'],
+ };
+ }
+}
+
+/**
+ * Use the backend {@link ExternalTerminalService} to establish the schema for the `ExternalTerminalPreferences`.
+ *
+ * @param externalTerminalService the external terminal backend service.
+ * @returns a preference schema with the OS default exec set by the backend service.
+ */
+export async function getExternalTerminalSchema(externalTerminalService: ExternalTerminalService): Promise {
+ const hostExec = await externalTerminalService.getDefaultExec();
+ return {
+ type: 'object',
+ properties: {
+ 'terminal.external.windowsExec': {
+ type: 'string',
+ description: 'Customizes which terminal to run on Windows.',
+ default: `${isWindows ? hostExec : 'C:\\WINDOWS\\System32\\cmd.exe'}`
+ },
+ 'terminal.external.osxExec': {
+ type: 'string',
+ description: 'Customizes which terminal to run on macOS.',
+ default: `${isOSX ? hostExec : 'Terminal.app'}`
+ },
+ 'terminal.external.linuxExec': {
+ type: 'string',
+ description: 'Customizes which terminal to run on Linux.',
+ default: `${!(isWindows || isOSX) ? hostExec : 'xterm'}`
+ }
+ }
+ };
+}
diff --git a/packages/external-terminal/src/electron-node/external-terminal-backend-module.ts b/packages/external-terminal/src/electron-node/external-terminal-backend-module.ts
new file mode 100644
index 0000000000000..0dbc5282a65d5
--- /dev/null
+++ b/packages/external-terminal/src/electron-node/external-terminal-backend-module.ts
@@ -0,0 +1,40 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { ContainerModule, interfaces } from 'inversify';
+import { ConnectionHandler, JsonRpcConnectionHandler } from '@theia/core/lib/common';
+import { isWindows, isOSX } from '@theia/core/lib/common/os';
+import { ExternalTerminalService, externalTerminalServicePath } from '../common/external-terminal';
+import { MacExternalTerminalService } from './mac-external-terminal-service';
+import { LinuxExternalTerminalService } from './linux-external-terminal-service';
+import { WindowsExternalTerminalService } from './windows-external-terminal-service';
+
+export function bindExternalTerminalService(bind: interfaces.Bind): void {
+ const serviceProvider: interfaces.ServiceIdentifier =
+ isWindows ? WindowsExternalTerminalService : isOSX ? MacExternalTerminalService : LinuxExternalTerminalService;
+ bind(serviceProvider).toSelf().inSingletonScope();
+ bind(ExternalTerminalService).toService(serviceProvider);
+
+ bind(ConnectionHandler).toDynamicValue(ctx =>
+ new JsonRpcConnectionHandler(externalTerminalServicePath, () =>
+ ctx.container.get(ExternalTerminalService)
+ )
+ ).inSingletonScope();
+}
+
+export default new ContainerModule(bind => {
+ bindExternalTerminalService(bind);
+});
diff --git a/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts
new file mode 100644
index 0000000000000..359b3bbf40191
--- /dev/null
+++ b/packages/external-terminal/src/electron-node/linux-external-terminal-service.ts
@@ -0,0 +1,95 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as cp from 'child_process';
+import * as fs from 'fs-extra';
+import { injectable } from 'inversify';
+import { OS } from '@theia/core/lib/common/os';
+import { FileUri } from '@theia/core/lib/node/file-uri';
+import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal';
+
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts
+
+@injectable()
+export class LinuxExternalTerminalService implements ExternalTerminalService {
+ protected DEFAULT_TERMINAL_LINUX_READY: Promise;
+
+ async openTerminal(configuration: ExternalTerminalConfiguration, cwd: string): Promise {
+ await this.spawnTerminal(configuration, FileUri.fsPath(cwd));
+ }
+
+ async getDefaultExec(): Promise {
+ return this.getDefaultTerminalLinux();
+ }
+
+ /**
+ * Spawn the external terminal for the given options.
+ * - The method spawns the terminal application based on the preferences, else uses the default value.
+ * @param configuration the preference configuration.
+ * @param cwd the optional current working directory to spawn from.
+ */
+ protected async spawnTerminal(configuration: ExternalTerminalConfiguration, cwd?: string): Promise {
+
+ // Use the executable value from the preferences if available, else fallback to the default.
+ const terminalConfig = configuration['terminal.external.linuxExec'];
+ const execPromise = terminalConfig ? Promise.resolve(terminalConfig) : this.getDefaultTerminalLinux();
+
+ return new Promise((resolve, reject) => {
+ execPromise.then(exec => {
+ const env = cwd ? { cwd } : undefined;
+ const child = cp.spawn(exec, [], env);
+ child.on('error', reject);
+ child.on('exit', resolve);
+ });
+ });
+ }
+
+ /**
+ * Get the default terminal application on Linux.
+ * - The following method uses environment variables to identify the best default possible for each distro.
+ *
+ * @returns the default application on Linux.
+ */
+ protected async getDefaultTerminalLinux(): Promise {
+ if (!this.DEFAULT_TERMINAL_LINUX_READY) {
+ this.DEFAULT_TERMINAL_LINUX_READY = new Promise(async resolve => {
+ if (OS.type() === OS.Type.Linux) {
+ const isDebian = await fs.pathExists('/etc/debian_version');
+ if (isDebian) {
+ resolve('x-terminal-emulator');
+ } else if (process.env.DESKTOP_SESSION === 'gnome' || process.env.DESKTOP_SESSION === 'gnome-classic') {
+ resolve('gnome-terminal');
+ } else if (process.env.DESKTOP_SESSION === 'kde-plasma') {
+ resolve('konsole');
+ } else if (process.env.COLORTERM) {
+ resolve(process.env.COLORTERM);
+ } else if (process.env.TERM) {
+ resolve(process.env.TERM);
+ } else {
+ resolve('xterm');
+ }
+ } else {
+ resolve('xterm');
+ }
+ });
+ }
+ return this.DEFAULT_TERMINAL_LINUX_READY;
+ }
+}
diff --git a/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts
new file mode 100644
index 0000000000000..81fc95ec7448a
--- /dev/null
+++ b/packages/external-terminal/src/electron-node/mac-external-terminal-service.ts
@@ -0,0 +1,70 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as cp from 'child_process';
+import { injectable } from 'inversify';
+import { FileUri } from '@theia/core/lib/node/file-uri';
+import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal';
+
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts
+
+@injectable()
+export class MacExternalTerminalService implements ExternalTerminalService {
+ protected osxOpener = '/usr/bin/open';
+ protected defaultTerminalApp = 'Terminal.app';
+
+ async openTerminal(configuration: ExternalTerminalConfiguration, cwd: string): Promise {
+ await this.spawnTerminal(configuration, FileUri.fsPath(cwd));
+ }
+
+ async getDefaultExec(): Promise {
+ return this.getDefaultTerminalOSX();
+ }
+
+ /**
+ * Spawn the external terminal for the given options.
+ * - The method spawns the terminal application based on the preferences, else uses the default value.
+ * @param configuration the preference configuration.
+ * @param cwd the optional current working directory to spawn from.
+ */
+ protected async spawnTerminal(configuration: ExternalTerminalConfiguration, cwd?: string): Promise {
+
+ // Use the executable value from the preferences if available, else fallback to the default.
+ const terminalConfig = configuration['terminal.external.osxExec'];
+ const terminalApp = terminalConfig || this.getDefaultTerminalOSX();
+
+ return new Promise((resolve, reject) => {
+ const args = ['-a', terminalApp];
+ if (cwd) {
+ args.push(cwd);
+ }
+ const child = cp.spawn(this.osxOpener, args);
+ child.on('error', reject);
+ child.on('exit', () => resolve());
+ });
+ }
+
+ /**
+ * Get the default terminal app on OSX.
+ */
+ protected getDefaultTerminalOSX(): string {
+ return this.defaultTerminalApp;
+ }
+}
diff --git a/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts
new file mode 100644
index 0000000000000..39c8fac73711f
--- /dev/null
+++ b/packages/external-terminal/src/electron-node/windows-external-terminal-service.ts
@@ -0,0 +1,110 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import * as cp from 'child_process';
+import * as path from 'path';
+import { injectable } from 'inversify';
+import { FileUri } from '@theia/core/lib/node/file-uri';
+import { ExternalTerminalService, ExternalTerminalConfiguration } from '../common/external-terminal';
+
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+// some code copied and modified from https://github.com/microsoft/vscode/blob/1.52.1/src/vs/workbench/contrib/externalTerminal/node/externalTerminalService.ts
+
+@injectable()
+export class WindowsExternalTerminalService implements ExternalTerminalService {
+ protected readonly CMD = 'cmd.exe';
+ protected DEFAULT_TERMINAL_WINDOWS: string;
+
+ async openTerminal(configuration: ExternalTerminalConfiguration, cwd: string): Promise {
+ await this.spawnTerminal(configuration, FileUri.fsPath(cwd));
+ }
+
+ async getDefaultExec(): Promise {
+ return this.getDefaultTerminalWindows();
+ }
+
+ /**
+ * Spawn the external terminal for the given options.
+ * - The method spawns the terminal application based on the preferences, else uses the default value.
+ * @param configuration the preference configuration.
+ * @param cwd the optional current working directory to spawn from.
+ */
+ protected async spawnTerminal(configuration: ExternalTerminalConfiguration, cwd?: string): Promise {
+
+ // Use the executable value from the preferences if available, else fallback to the default.
+ const terminalConfig = configuration['terminal.external.windowsExec'];
+ const exec = terminalConfig || this.getDefaultTerminalWindows();
+
+ // Make the drive letter uppercase on Windows (https://github.com/microsoft/vscode/issues/9448).
+ if (cwd && cwd[1] === ':') {
+ cwd = cwd[0].toUpperCase() + cwd.substr(1);
+ }
+
+ // cmder ignores the environment cwd and instead opts to always open in %USERPROFILE%
+ // unless otherwise specified.
+ const basename = path.basename(exec).toLowerCase();
+ if (basename === 'cmder' || basename === 'cmder.exe') {
+ cp.spawn(exec, cwd ? [cwd] : undefined);
+ return;
+ }
+
+ const cmdArgs = ['/c', 'start', '/wait'];
+ // The "" argument is the window title. Without this, exec doesn't work when the path contains spaces.
+ if (exec.indexOf(' ') >= 0) {
+ cmdArgs.push('""');
+ }
+
+ cmdArgs.push(exec);
+
+ // Add starting directory parameter for Windows Terminal app.
+ if (basename === 'wt' || basename === 'wt.exe') {
+ cmdArgs.push('-d .');
+ }
+
+ return new Promise(async (resolve, reject) => {
+ const env = cwd ? { cwd } : undefined;
+ const command = this.getWindowsShell();
+ const child = cp.spawn(command, cmdArgs, env);
+ child.on('error', reject);
+ child.on('exit', resolve);
+ });
+ }
+
+ /**
+ * Get the default terminal application on Windows.
+ * - The following method uses environment variables to identify the best default possible value.
+ *
+ * @returns the default application on Windows.
+ */
+ protected getDefaultTerminalWindows(): string {
+ if (!this.DEFAULT_TERMINAL_WINDOWS) {
+ const isWoW64 = !!process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432');
+ this.DEFAULT_TERMINAL_WINDOWS = `${process.env.windir ? process.env.windir : 'C:\\Windows'}\\${isWoW64 ? 'Sysnative' : 'System32'}\\cmd.exe`;
+ }
+ return this.DEFAULT_TERMINAL_WINDOWS;
+ }
+
+ /**
+ * Find the Windows Shell process to start up (defaults to cmd.exe).
+ */
+ protected getWindowsShell(): string {
+ // Find the path to cmd.exe if possible (%compsec% environment variable).
+ return process.env.compsec || this.CMD;
+ }
+}
diff --git a/packages/external-terminal/src/package.spec.ts b/packages/external-terminal/src/package.spec.ts
new file mode 100644
index 0000000000000..7cce78b8c9b86
--- /dev/null
+++ b/packages/external-terminal/src/package.spec.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+/* note: this bogus test file is required so that
+ we are able to run mocha unit tests on this
+ package, without having any actual unit tests in it.
+ This way a coverage report will be generated,
+ showing 0% coverage, instead of no report.
+ This file can be removed once we have real unit
+ tests in place. */
+
+describe('external-terminal package', () => {
+
+ it('support code coverage statistics', () => true);
+
+});
diff --git a/tsconfig.json b/tsconfig.json
index a3b9decb887d7..3ef7724ac0815 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -58,6 +58,9 @@
"@theia/editor/lib/*": [
"packages/editor/src/*"
],
+ "@theia/external-terminal/lib/*": [
+ "packages/external-terminal/src/*"
+ ],
"@theia/file-search/lib/*": [
"packages/file-search/src/*"
],