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 @@ +
+ +
+ +theia-ext-logo + +

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/*" ],