Skip to content

Commit c89ac1a

Browse files
committed
[vscode] Support TerminalState shell property
fixes #15433 Contributed on behalf of STMicroelectronics Signed-off-by: Remi Schnekenburger <rschnekenburger@eclipsesource.com>
1 parent f9631e9 commit c89ac1a

File tree

7 files changed

+127
-7
lines changed

7 files changed

+127
-7
lines changed

packages/plugin-ext/src/common/plugin-api-rpc.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,8 @@ export interface TerminalServiceExt {
312312
$terminalOnInput(id: string, data: string): void;
313313
$terminalSizeChanged(id: string, cols: number, rows: number): void;
314314
$currentTerminalChanged(id: string | undefined): void;
315-
$terminalStateChanged(id: string): void;
315+
$terminalOnInteraction(id: string): void;
316+
$terminalShellTypeChanged(id: string, newShellType: string): void;
316317
$initEnvironmentVariableCollections(collections: [string, string, boolean, SerializableEnvironmentVariableCollection][]): void;
317318
$provideTerminalLinks(line: string, terminalId: string, token: theia.CancellationToken): Promise<ProvidedTerminalLink[]>;
318319
$handleTerminalLink(link: ProvidedTerminalLink): Promise<void>;

packages/plugin-ext/src/main/browser/terminal-main.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,12 @@ export class TerminalServiceMainImpl implements TerminalServiceMain, TerminalLin
130130
}));
131131
this.toDispose.push(terminal.onData(data => {
132132
this.extProxy.$terminalOnInput(terminal.id, data);
133-
this.extProxy.$terminalStateChanged(terminal.id);
133+
this.extProxy.$terminalOnInteraction(terminal.id);
134134
}));
135135

136+
this.toDispose.push(terminal.onShellTypeChanged(shellType => {
137+
this.extProxy.$terminalShellTypeChanged(terminal.id, shellType);
138+
}));
136139
this.observers.forEach((observer, id) => this.observeTerminal(id, terminal, observer));
137140
}
138141

packages/plugin-ext/src/plugin/terminal-ext.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,24 @@ export class TerminalServiceExtImpl implements TerminalServiceExt {
161161
terminal.emitOnInput(data);
162162
}
163163

164-
$terminalStateChanged(id: string): void {
164+
$terminalOnInteraction(id: string): void {
165165
const terminal = this._terminals.get(id);
166166
if (!terminal) {
167167
return;
168168
}
169169
if (!terminal.state.isInteractedWith) {
170-
terminal.state = { isInteractedWith: true };
170+
terminal.state = { ...terminal.state, isInteractedWith: true };
171+
this.onDidChangeTerminalStateEmitter.fire(terminal);
172+
}
173+
}
174+
175+
$terminalShellTypeChanged(id: string, shellType: string): void {
176+
const terminal = this._terminals.get(id);
177+
if (!terminal) {
178+
return;
179+
}
180+
if (terminal.state.shell !== shellType) {
181+
terminal.state = { ...terminal.state, shell: shellType };
171182
this.onDidChangeTerminalStateEmitter.fire(terminal);
172183
}
173184
}
@@ -472,7 +483,7 @@ export class TerminalExtImpl implements theia.Terminal {
472483

473484
readonly creationOptions: Readonly<theia.TerminalOptions | theia.ExtensionTerminalOptions>;
474485

475-
state: theia.TerminalState = { isInteractedWith: false };
486+
state: theia.TerminalState = { isInteractedWith: false, shell: undefined };
476487

477488
constructor(private readonly proxy: TerminalServiceMain, private readonly options: theia.TerminalOptions | theia.ExtensionTerminalOptions) {
478489
this.creationOptions = this.options;

packages/plugin/src/theia.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -3146,6 +3146,21 @@ export module '@theia/plugin' {
31463146
* https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
31473147
*/
31483148
readonly isInteractedWith: boolean;
3149+
3150+
/**
3151+
* The detected shell type of the {@link Terminal}. This will be `undefined` when there is
3152+
* not a clear signal as to what the shell is, or the shell is not supported yet. This
3153+
* value should change to the shell type of a sub-shell when launched (for example, running
3154+
* `bash` inside `zsh`).
3155+
*
3156+
* Note that current implementation only assess the shell type on terminal creation, and it is
3157+
* not updated if a sub-shell is currnetly launched.
3158+
*
3159+
* Note that the possible values are currently defined as any of the following:
3160+
* 'bash', 'cmd', 'csh', 'fish', 'gitbash', 'julia', 'ksh', 'node', 'nu', 'pwsh', 'python',
3161+
* 'sh', 'wsl', 'zsh'.
3162+
*/
3163+
readonly shell: string | undefined;
31493164
}
31503165

31513166
/**

packages/terminal/src/browser/base/terminal-widget.ts

+3
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ export abstract class TerminalWidget extends BaseWidget {
128128
/** Event that fires when the terminal input data */
129129
abstract onData: Event<string>;
130130

131+
/** Event that fires when the terminal shell type is changed */
132+
abstract onShellTypeChanged: Event<string>;
133+
131134
abstract onOutput: Event<string>;
132135

133136
abstract buffer: TerminalBuffer;

packages/terminal/src/browser/terminal-widget-impl.ts

+53-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import { EnhancedPreviewWidget } from '@theia/core/lib/browser/widgets/enhanced-
5050
import { MarkdownRenderer, MarkdownRendererFactory } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer';
5151
import { RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
5252
import { ColorRegistry } from '@theia/core/lib/browser/color-registry';
53+
import { GeneralShellType, WindowsShellType } from '../common/terminal';
54+
import * as path from 'path';
5355

5456
export const TERMINAL_WIDGET_FACTORY_ID = 'terminal';
5557

@@ -157,6 +159,9 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
157159
protected readonly onMouseLeaveLinkHoverEmitter = new Emitter<MouseEvent>();
158160
readonly onMouseLeaveLinkHover: Event<MouseEvent> = this.onMouseLeaveLinkHoverEmitter.event;
159161

162+
protected readonly onShellTypeChangedEmiter = new Emitter<string>();
163+
readonly onShellTypeChanged: Event<string> = this.onShellTypeChangedEmiter.event;
164+
160165
protected readonly toDisposeOnConnect = new DisposableCollection();
161166

162167
private _buffer: TerminalBuffer;
@@ -260,6 +265,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
260265
this.toDispose.push(this.onSizeChangedEmitter);
261266
this.toDispose.push(this.onDataEmitter);
262267
this.toDispose.push(this.onKeyEmitter);
268+
this.toDispose.push(this.onShellTypeChangedEmiter);
263269

264270
const touchEndListener = (event: TouchEvent) => {
265271
if (this.node.contains(event.target as Node)) {
@@ -589,7 +595,6 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
589595
rootURI = root?.resource?.toString();
590596
}
591597
const { cols, rows } = this.term;
592-
593598
const terminalId = await this.shellTerminalServer.create({
594599
shell: this.options.shellPath || this.shellPreferences.shell[OS.backend.type()],
595600
args: this.options.shellArgs || this.shellPreferences.shellArgs[OS.backend.type()],
@@ -601,6 +606,11 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
601606
rows
602607
});
603608
if (IBaseTerminalServer.validateId(terminalId)) {
609+
const processInfo = await this.shellTerminalServer.getProcessInfo(terminalId);
610+
const shellType = guessShellTypeFromExecutable(processInfo.executable);
611+
if (shellType) {
612+
this.onShellTypeChangedEmiter.fire(shellType);
613+
}
604614
return terminalId;
605615
}
606616
throw new Error('Error creating terminal widget, see the backend error log for more information.');
@@ -675,7 +685,7 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
675685
const waitForConnection = this.waitForConnection = new Deferred<Channel>();
676686
this.connectionProvider.listen(
677687
`${terminalsPath}/${this.terminalId}`,
678-
(path, connection) => {
688+
(_path, connection) => {
679689
connection.onMessage(e => {
680690
this.write(e().readString());
681691
});
@@ -995,3 +1005,44 @@ export class TerminalWidgetImpl extends TerminalWidget implements StatefulWidget
9951005
return this.enhancedPreviewNode;
9961006
}
9971007
}
1008+
1009+
function guessShellTypeFromExecutable(executable: string | undefined): string | undefined {
1010+
if (!executable) {
1011+
return undefined;
1012+
}
1013+
1014+
const executableName = path.basename(executable);
1015+
// windows tested first, as gitbash may be confused with other OS bash
1016+
if (OS.backend.isWindows) {
1017+
const windowShellTypesToRegex: Map<string, RegExp> = new Map([
1018+
[WindowsShellType.CommandPrompt, /^cmd$/],
1019+
[WindowsShellType.GitBash, /^bash$/],
1020+
[WindowsShellType.Wsl, /^wsl$/]
1021+
]);
1022+
for (const [shellType, pattern] of windowShellTypesToRegex) {
1023+
if (executableName.match(pattern)) {
1024+
return shellType;
1025+
}
1026+
}
1027+
}
1028+
1029+
const shellTypesToRegex: Map<string, RegExp> = new Map([
1030+
[GeneralShellType.Bash, /^bash$/],
1031+
[GeneralShellType.Csh, /^csh$/],
1032+
[GeneralShellType.Fish, /^fish$/],
1033+
[GeneralShellType.Julia, /^julia$/],
1034+
[GeneralShellType.Ksh, /^ksh$/],
1035+
[GeneralShellType.Node, /^node$/],
1036+
[GeneralShellType.NuShell, /^nu$/],
1037+
[GeneralShellType.PowerShell, /^pwsh(-preview)?|powershell$/],
1038+
[GeneralShellType.Python, /^py(?:thon)?$/],
1039+
[GeneralShellType.Sh, /^sh$/],
1040+
[GeneralShellType.Zsh, /^zsh$/]
1041+
]);
1042+
for (const [shellType, pattern] of shellTypesToRegex) {
1043+
if (executableName.match(pattern)) {
1044+
return shellType;
1045+
}
1046+
}
1047+
return undefined;
1048+
}
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 STMicroelectronics and others.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
export const enum GeneralShellType {
17+
Bash = 'bash',
18+
Csh = 'csh',
19+
Fish = 'fish',
20+
Julia = 'julia',
21+
Ksh = 'ksh',
22+
Node = 'node',
23+
NuShell = 'nu',
24+
PowerShell = 'pwsh',
25+
Python = 'python',
26+
Sh = 'sh',
27+
Zsh = 'zsh',
28+
}
29+
30+
export const enum WindowsShellType {
31+
CommandPrompt = 'cmd',
32+
GitBash = 'gitbash',
33+
Wsl = 'wsl'
34+
}
35+
36+
export type ShellType = GeneralShellType | WindowsShellType;

0 commit comments

Comments
 (0)