Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cascadia/TerminalApp/TerminalPage.Protocol.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ namespace winrt::TerminalApp::implementation
if (const auto termControl = effectivePane->GetTerminalControl())
{
result.Cwd = termControl.WorkingDirectory();
result.Shell = termControl.ShellName();
result.ShellVersion = termControl.ShellVersion();
}

result.Pid = _getPidFromPane(effectivePane);
Expand Down Expand Up @@ -256,6 +258,8 @@ namespace winrt::TerminalApp::implementation
info.Rows = termControl.ViewHeight();
info.Columns = 0;
info.Cwd = termControl.WorkingDirectory();
info.Shell = termControl.ShellName();
info.ShellVersion = termControl.ShellVersion();
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1522,6 +1522,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return hstring{ _terminal->GetWorkingDirectory() };
}

hstring ControlCore::ShellName() const
{
const auto lock = _terminal->LockForReading();
return hstring{ _terminal->GetShellName() };
}

hstring ControlCore::ShellVersion() const
{
const auto lock = _terminal->LockForReading();
return hstring{ _terminal->GetShellVersion() };
}

bool ControlCore::BracketedPasteEnabled() const noexcept
{
const auto lock = _terminal->LockForReading();
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/ControlCore.h
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
hstring Title();
Windows::Foundation::IReference<winrt::Windows::UI::Color> TabColor() noexcept;
hstring WorkingDirectory() const;
hstring ShellName() const;
hstring ShellVersion() const;

TerminalConnection::ConnectionState ConnectionState() const;

Expand Down
6 changes: 6 additions & 0 deletions src/cascadia/TerminalControl/ICoreState.idl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ namespace Microsoft.Terminal.Control

String WorkingDirectory { get; };

// Shell identity reported by shell integration via OSC 9001;ShellType.
// Empty when the shell has not reported (no shell integration, or a
// shell that does not emit the sequence).
String ShellName { get; };
String ShellVersion { get; };

Windows.Foundation.IReference<Windows.UI.Color> TabColor { get; };

Int32 ScrollOffset { get; };
Expand Down
10 changes: 10 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation
return _core.WorkingDirectory();
}

hstring TermControl::ShellName() const
{
return _core.ShellName();
}

hstring TermControl::ShellVersion() const
{
return _core.ShellVersion();
}

bool TermControl::BracketedPasteEnabled() const noexcept
{
return _core.BracketedPasteEnabled();
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
hstring Title();
Windows::Foundation::IReference<winrt::Windows::UI::Color> TabColor() noexcept;
hstring WorkingDirectory() const;
hstring ShellName() const;
hstring ShellVersion() const;

TerminalConnection::ConnectionState ConnectionState() const;

Expand Down
10 changes: 10 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,16 @@ std::wstring_view Terminal::GetWorkingDirectory() noexcept
return _workingDirectory;
}

std::wstring_view Terminal::GetShellName() const noexcept
{
return _shellName;
}

std::wstring_view Terminal::GetShellVersion() const noexcept
{
return _shellVersion;
}

// Method Description:
// - Resize the terminal as the result of some user interaction.
// Arguments:
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class Microsoft::Terminal::Core::Terminal final :
void SetOptionalFeatures(winrt::Microsoft::Terminal::Core::ICoreSettings settings);
bool IsXtermBracketedPasteModeEnabled() const noexcept;
std::wstring_view GetWorkingDirectory() noexcept;
std::wstring_view GetShellName() const noexcept;
std::wstring_view GetShellVersion() const noexcept;

til::point GetViewportRelativeCursorPosition() const noexcept;

Expand Down Expand Up @@ -152,6 +154,7 @@ class Microsoft::Terminal::Core::Terminal final :
void CopyToClipboard(wil::zwstring_view content) override;
void SetTaskbarProgress(const ::Microsoft::Console::VirtualTerminal::DispatchTypes::TaskbarState state, const size_t progress) override;
void SetWorkingDirectory(std::wstring_view uri) override;
void SetShellType(std::wstring_view shellName, std::wstring_view shellVersion) override;
void PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration) override;
void ShowWindow(bool showOrHide) override;
void UseAlternateScreenBuffer(const TextAttribute& attrs) override;
Expand Down Expand Up @@ -382,6 +385,8 @@ class Microsoft::Terminal::Core::Terminal final :

std::wstring _answerbackMessage;
std::wstring _workingDirectory;
std::wstring _shellName;
std::wstring _shellVersion;
bool _highContrastMode = false;

// This default fake font value is only used to check if the font is a raster font.
Expand Down
25 changes: 25 additions & 0 deletions src/cascadia/TerminalCore/TerminalApi.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#include "../src/inc/unicode.hpp"

#include <atomic>

using namespace Microsoft::Terminal::Core;
using namespace Microsoft::Console::Render;
using namespace Microsoft::Console::Types;
Expand Down Expand Up @@ -234,6 +236,29 @@ void Terminal::SetWorkingDirectory(std::wstring_view uri)
_workingDirectory = uri;
}

void Terminal::SetShellType(std::wstring_view shellName, std::wstring_view shellVersion)
{
_assertLocked();

// Telemetry is logged once per process. `logged` is function-static
// (shared across all Terminal instances, which hold *different* locks),
// so the `_assertLocked()` above does NOT serialize it — use an atomic
// exchange to make the one-shot gate race-free.
static std::atomic<bool> logged{ false };
Comment thread
vanzue marked this conversation as resolved.
Comment thread
vanzue marked this conversation as resolved.
if (!logged.exchange(true))
{
TraceLoggingWrite(
g_hCTerminalCoreProvider,
"ShellIntegrationShellTypeSet",
TraceLoggingDescription("The shell reported its identity via OSC 9001;ShellType"),
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServiceUsage));
}

_shellName = shellName;
_shellVersion = shellVersion;
}

void Terminal::PlayMidiNote(const int noteNumber, const int velocity, const std::chrono::microseconds duration)
{
_pfnPlayMidiNote(noteNumber, velocity, duration);
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalProtocol/TerminalProtocol.idl
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ namespace Microsoft.Terminal.Protocol
// terminal control's working-directory tracking (OSC 9;9 / shell
// integration). Empty when not known.
String Cwd;
// Shell identity reported by shell integration via OSC 9001;ShellType
// (e.g. "pwsh", "powershell", "bash", "wsl:Ubuntu"). Empty when the
// shell has not reported its identity.
String Shell;
String ShellVersion;
};

struct PaneOutput
Expand Down
71 changes: 71 additions & 0 deletions src/cascadia/UnitTests_TerminalCore/ShellIntegrationTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class TerminalCoreUnitTests::ShellIntegrationTests final
TEST_METHOD(Install_IdempotentWhenAlreadyInstalled);
TEST_METHOD(Install_ReinstallsWhenScriptMissingButBlockMatches);
TEST_METHOD(Install_RewritesLegacyDotSourceLineInPlace);
TEST_METHOD(Install_UpgradesWhenBlockReferencesOlderScriptVersion);
TEST_METHOD(Install_OverwritesOrphanOpenMarker);
TEST_METHOD(Install_CreatesBackupForNonEmptyProfile);
TEST_METHOD(Install_DoesNotCreateBackupForEmptyProfile);
Expand Down Expand Up @@ -119,6 +120,7 @@ class TerminalCoreUnitTests::ShellIntegrationTests final
TEST_METHOD(Bash_Install_IsLfOnly);
TEST_METHOD(Bash_Install_IdempotentWhenAlreadyInstalled);
TEST_METHOD(Bash_Install_ReinstallsWhenScriptMissingButBlockMatches);
TEST_METHOD(Bash_Install_UpgradesWhenBlockReferencesOlderScriptVersion);
TEST_METHOD(Bash_Install_OverwritesOrphanOpenMarker);
TEST_METHOD(Bash_Install_CreatesBackupForNonEmptyProfile);
TEST_METHOD(Bash_Install_DoesNotCreateBackupForEmptyProfile);
Expand Down Expand Up @@ -546,6 +548,43 @@ void ShellIntegrationTests::Install_RewritesLegacyDotSourceLineInPlace()
VERIFY_IS_TRUE(blockPos < tailPos, L"In-place rewrite — block stays where legacy line was");
}

void ShellIntegrationTests::Install_UpgradesWhenBlockReferencesOlderScriptVersion()
{
// Regression: bumping the script version (e.g. v1 -> v2 when OSC
// 9001;ShellType emission was added) must actually reach existing
// users. Their $PROFILE already has a well-formed managed block, but it
// references the OLDER versioned script filename and the stale old
// script sits on disk. The block-match early-out must NOT fire (the
// block no longer equals the desired, current-version block), so the
// current script is written and the block is rewritten to point at it.
const auto profile = _ProfilePath();
const auto currentName = til::u16u8(ShellIntegrationScriptFileName());

// Simulate a prior install: take the current block and point it at an
// older script version. v0 is always older than any shipped vN, so this
// stays valid across future bumps.
const std::string oldName = "shell-integration_v0.ps1";
auto oldBlock = BuildShellIntegrationBlock(L"PowerShell", "\n");
const auto namePos = oldBlock.find(currentName);
VERIFY_ARE_NOT_EQUAL(std::string::npos, namePos, L"Block must embed the current script filename");
oldBlock.replace(namePos, currentName.size(), oldName);
_WriteFile(profile, oldBlock + "\n");
// Stale old script on disk, without the ShellType emission.
_WriteFile(profile.parent_path() / L"shell-integration_v0.ps1", "# stale old script, no ShellType\n");

const auto r = Install(profile.wstring());
VERIFY_IS_TRUE(r.success);
VERIFY_IS_FALSE(r.alreadyInstalled, L"Block referenced an older script version → must upgrade, not no-op");

const auto contents = _ReadFile(profile);
VERIFY_IS_TRUE(_Contains(contents, currentName), L"Block must be rewritten to the current script version");
VERIFY_IS_FALSE(_Contains(contents, oldName), L"Old version reference must be replaced");

const auto scriptPath = profile.parent_path() / ShellIntegrationScriptFileName();
VERIFY_IS_TRUE(std::filesystem::exists(scriptPath), L"Current-version script must be written on upgrade");
VERIFY_IS_TRUE(_Contains(_ReadFile(scriptPath), "9001"), L"Upgraded script must emit OSC 9001;ShellType");
}

void ShellIntegrationTests::Install_OverwritesOrphanOpenMarker()
{
const auto profile = _ProfilePath();
Expand Down Expand Up @@ -1210,6 +1249,38 @@ void ShellIntegrationTests::Bash_Install_ReinstallsWhenScriptMissingButBlockMatc
VERIFY_IS_TRUE(std::filesystem::exists(scriptPath));
}

void ShellIntegrationTests::Bash_Install_UpgradesWhenBlockReferencesOlderScriptVersion()
{
// Bash/WSL counterpart of the PowerShell upgrade regression: an existing
// ~/.bashrc managed block that references an OLDER versioned script must
// be rewritten to the current version (so the OSC 9001;ShellType emission
// added in v2 actually reaches users who already had v1 installed).
const auto profile = _BashProfilePath();
const auto scriptDir = _BashScriptDir();
const auto currentName = til::u16u8(ShellIntegrationBashScriptFileName());

const std::string oldName = "shell-integration_v0.sh";
auto oldBlock = BuildShellIntegrationBashBlock();
const auto namePos = oldBlock.find(currentName);
VERIFY_ARE_NOT_EQUAL(std::string::npos, namePos, L"Block must embed the current script filename");
oldBlock.replace(namePos, currentName.size(), oldName);
_WriteFile(profile, oldBlock + "\n");
// Stale old script on disk, without the ShellType emission.
_WriteFile(scriptDir / L"shell-integration_v0.sh", "# stale old script, no ShellType\n");

const auto r = InstallBash(profile.wstring(), scriptDir.wstring());
VERIFY_IS_TRUE(r.success);
VERIFY_IS_FALSE(r.alreadyInstalled, L"Block referenced an older script version → must upgrade, not no-op");

const auto contents = _ReadFile(profile);
VERIFY_IS_TRUE(_Contains(contents, currentName), L"Block must be rewritten to the current script version");
VERIFY_IS_FALSE(_Contains(contents, oldName), L"Old version reference must be replaced");

const auto scriptPath = scriptDir / ShellIntegrationBashScriptFileName();
VERIFY_IS_TRUE(std::filesystem::exists(scriptPath), L"Current-version script must be written on upgrade");
VERIFY_IS_TRUE(_Contains(_ReadFile(scriptPath), "9001"), L"Upgraded script must emit OSC 9001;ShellType");
}

void ShellIntegrationTests::Bash_Install_OverwritesOrphanOpenMarker()
{
const auto profile = _BashProfilePath();
Expand Down
42 changes: 42 additions & 0 deletions src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ namespace TerminalCoreUnitTests

TEST_METHOD(SetTaskbarProgress);
TEST_METHOD(SetWorkingDirectory);
TEST_METHOD(SetShellType);
};
};

Expand Down Expand Up @@ -391,3 +392,44 @@ void TerminalCoreUnitTests::TerminalApiTest::SetWorkingDirectory()
stateMachine.ProcessString(L"\x1b]9;9;D:\\中文\x1b\\");
VERIFY_ARE_EQUAL(term.GetWorkingDirectory(), L"D:\\中文");
}

void TerminalCoreUnitTests::TerminalApiTest::SetShellType()
{
Terminal term{ Terminal::TestDummyMarker{} };
DummyRenderer renderer{ &term };
term.Create({ 100, 100 }, 0, renderer);

auto& stateMachine = *(term._stateMachine);

// The shell reports its identity via OSC 9001;ShellType;<name>;<version>.
// Initially nothing has been reported.
VERIFY_IS_TRUE(term.GetShellName().empty());
VERIFY_IS_TRUE(term.GetShellVersion().empty());

// Name + version both present.
stateMachine.ProcessString(L"\x1b]9001;ShellType;pwsh;7.4.1\x1b\\");
VERIFY_ARE_EQUAL(term.GetShellName(), L"pwsh");
VERIFY_ARE_EQUAL(term.GetShellVersion(), L"7.4.1");

// A later prompt from a different shell (e.g. after `wsl` exits) overwrites
// the previously reported identity — last writer wins.
stateMachine.ProcessString(L"\x1b]9001;ShellType;wsl:Ubuntu;5.15.0\x1b\\");
VERIFY_ARE_EQUAL(term.GetShellName(), L"wsl:Ubuntu");
VERIFY_ARE_EQUAL(term.GetShellVersion(), L"5.15.0");

// Version is optional — name only still updates the name and clears version.
stateMachine.ProcessString(L"\x1b]9001;ShellType;bash\x1b\\");
VERIFY_ARE_EQUAL(term.GetShellName(), L"bash");
VERIFY_IS_TRUE(term.GetShellVersion().empty());

// A ShellType action with no name field is tolerated and clears both.
stateMachine.ProcessString(L"\x1b]9001;ShellType\x1b\\");
VERIFY_IS_TRUE(term.GetShellName().empty());
VERIFY_IS_TRUE(term.GetShellVersion().empty());

// An unrelated WT sub-action must not touch the stored shell identity.
stateMachine.ProcessString(L"\x1b]9001;ShellType;powershell;5.1\x1b\\");
stateMachine.ProcessString(L"\x1b]9001;CmdNotFound;somecmd\x1b\\");
VERIFY_ARE_EQUAL(term.GetShellName(), L"powershell");
VERIFY_ARE_EQUAL(term.GetShellVersion(), L"5.1");
}
2 changes: 2 additions & 0 deletions src/cascadia/WindowsTerminal/TerminalProtocolComServer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ static Json::Value _toJson(const Protocol::PaneInfo& p)
v["size"]["rows"] = p.Rows;
v["size"]["columns"] = p.Columns;
v["cwd"] = winrt::to_string(p.Cwd);
v["shell"] = winrt::to_string(p.Shell);
v["shell_version"] = winrt::to_string(p.ShellVersion);
return v;
}

Expand Down
17 changes: 16 additions & 1 deletion src/cascadia/inc/BashShellIntegration.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@

namespace Microsoft::Terminal::ShellIntegration::Bash
{
inline constexpr int kVersion = 1;
// v2: added OSC 9001;ShellType emission (bash/WSL self-reports identity
// each prompt). Bumped from v1 so existing users — whose ~/.bashrc
// already references the v1 script byte-for-byte — get the new script
// rewritten in; without the bump the orchestrator's block-match early-
// out would leave the stale v1 script (no ShellType) in place. WSL
// inherits this version via WslBashFlavor.
inline constexpr int kVersion = 2;

inline std::wstring ScriptFileName()
{
Expand Down Expand Up @@ -143,6 +149,15 @@ __it_shellinteg_prompt() {
# CWD reporting for those directories. The unquoted form parses
# cleanly regardless of path contents.
printf '\033]133;D;%s\007\033]133;A\007\033]9;9;%s\007' "$__ec" "${PWD:-}"
# OSC 9001;ShellType — report shell identity each prompt so the terminal
# always knows which shell owns the pane, even after a nested shell exits.
# Under WSL, $WSL_DISTRO_NAME is set so we report "wsl:<distro>"; plain
# (Git) bash reports "bash".
if [ -n "${WSL_DISTRO_NAME:-}" ]; then
printf '\033]9001;ShellType;wsl:%s;%s\007' "$WSL_DISTRO_NAME" "${BASH_VERSION:-}"
else
printf '\033]9001;ShellType;bash;%s\007' "${BASH_VERSION:-}"
fi
if [ -n "$__IT_SHELLINTEG_USER_PC" ]; then
# Restore $? for the user's PROMPT_COMMAND so hooks like
# `local ec=$?` at its top still see the real exit code
Expand Down
Loading
Loading