From 7a84c10bdc6947a0a2b9375bf93677dd9091cedf Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 25 Mar 2026 21:49:45 +0100 Subject: [PATCH 1/3] Implement OSC 7 for setting the CWD --- .../TerminalApp/AppActionHandlers.cpp | 2 +- src/cascadia/TerminalApp/TerminalPage.cpp | 6 ++---- .../TerminalApp/TerminalPaneContent.cpp | 2 +- src/cascadia/TerminalControl/ControlCore.cpp | 5 ----- src/cascadia/TerminalControl/ControlCore.h | 2 -- src/cascadia/TerminalControl/ICoreState.idl | 2 -- src/cascadia/TerminalControl/TermControl.cpp | 4 ---- src/cascadia/TerminalControl/TermControl.h | 2 -- src/common.build.pre.props | 2 +- src/terminal/adapter/ITermDispatch.hpp | 5 +---- src/terminal/adapter/adaptDispatch.cpp | 21 +++++++++++++++++++ src/terminal/adapter/adaptDispatch.hpp | 1 + src/terminal/adapter/precomp.h | 3 ++- src/terminal/adapter/termDispatch.hpp | 1 + .../parser/OutputStateMachineEngine.cpp | 3 +-- src/types/inc/utils.hpp | 2 ++ src/types/utils.cpp | 13 ++++++++++++ 17 files changed, 47 insertions(+), 29 deletions(-) diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp index 48ed69822a2..66372a3e9e9 100644 --- a/src/cascadia/TerminalApp/AppActionHandlers.cpp +++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp @@ -1475,7 +1475,7 @@ namespace winrt::TerminalApp::implementation WI_IsAnyFlagSet(source, SuggestionsSource::CommandHistory | SuggestionsSource::QuickFixes); if (const auto& control{ _GetActiveControl() }) { - currentWorkingDirectory = control.CurrentWorkingDirectory(); + currentWorkingDirectory = control.WorkingDirectory(); if (shouldGetContext) { diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index e98de6b9957..b061f7bc63a 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1600,8 +1600,7 @@ namespace winrt::TerminalApp::implementation // Replace the Starting directory with the CWD, if given const auto workingDirectory = control.WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) + if (Utils::IsValidDirectory(workingDirectory.c_str())) { controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); } @@ -3555,8 +3554,7 @@ namespace winrt::TerminalApp::implementation profile = GetClosestProfileForDuplicationOfProfile(profile); controlSettings = Settings::TerminalSettings::CreateWithProfile(_settings, profile); const auto workingDirectory = tabImpl->GetActiveTerminalControl().WorkingDirectory(); - const auto validWorkingDirectory = !workingDirectory.empty(); - if (validWorkingDirectory) + if (Utils::IsValidDirectory(workingDirectory.c_str())) { controlSettings.DefaultSettings()->StartingDirectory(workingDirectory); } diff --git a/src/cascadia/TerminalApp/TerminalPaneContent.cpp b/src/cascadia/TerminalApp/TerminalPaneContent.cpp index b7000d9c2d9..3a2c487db90 100644 --- a/src/cascadia/TerminalApp/TerminalPaneContent.cpp +++ b/src/cascadia/TerminalApp/TerminalPaneContent.cpp @@ -102,7 +102,7 @@ namespace winrt::TerminalApp::implementation args.Profile(::Microsoft::Console::Utils::GuidToString(_profile.Guid())); // If we know the user's working directory use it instead of the profile. - if (const auto dir = _control.WorkingDirectory(); !dir.empty()) + if (const auto dir = _control.WorkingDirectory(); ::Microsoft::Console::Utils::IsValidDirectory(dir.c_str())) { args.StartingDirectory(dir); } diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index f10847fbfeb..eb84587d70b 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2420,11 +2420,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation return *context; } - winrt::hstring ControlCore::CurrentWorkingDirectory() const - { - return winrt::hstring{ _terminal->GetWorkingDirectory() }; - } - bool ControlCore::QuickFixesAvailable() const noexcept { return _cachedQuickFixes && _cachedQuickFixes.Size() > 0; diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 690f6fb465b..06f39a0e2e3 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -191,8 +191,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ContextMenuSelectCommand(); void ContextMenuSelectOutput(); - - winrt::hstring CurrentWorkingDirectory() const; #pragma endregion #pragma region ITerminalInput diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl index 38f0cea30ae..f03d0391cad 100644 --- a/src/cascadia/TerminalControl/ICoreState.idl +++ b/src/cascadia/TerminalControl/ICoreState.idl @@ -60,7 +60,5 @@ namespace Microsoft.Terminal.Control void SelectOutput(Boolean goUp); IVector ScrollMarks { get; }; - String CurrentWorkingDirectory { get; }; - }; } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 3bd8ed5bbce..4e4a5a4b65f 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -3713,10 +3713,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return _core.CommandHistory(); } - winrt::hstring TermControl::CurrentWorkingDirectory() const - { - return _core.CurrentWorkingDirectory(); - } void TermControl::UpdateWinGetSuggestions(Windows::Foundation::Collections::IVector suggestions) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 3db3cec6730..c6a1eb55d6f 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -117,8 +117,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void ScrollToMark(const Control::ScrollToMarkDirection& direction); void SelectCommand(const bool goUp); void SelectOutput(const bool goUp); - - winrt::hstring CurrentWorkingDirectory() const; #pragma endregion void ScrollViewport(int viewTop); diff --git a/src/common.build.pre.props b/src/common.build.pre.props index b3f57bf2302..e8084905fcc 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -95,7 +95,7 @@ - v143 + v145 Unicode false x64 diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index 582d85b6d48..327aa09e130 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -82,6 +82,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual void BackIndex() = 0; // DECBI virtual void ForwardIndex() = 0; // DECFI virtual void SetWindowTitle(std::wstring_view title) = 0; // DECSWT, OscWindowTitle + virtual void SetCurrentWorkingDirectory(const std::wstring_view uri) = 0; // OSC 7 virtual void HorizontalTabSet() = 0; // HTS virtual void ForwardTab(const VTInt numTabs) = 0; // CHT, HT virtual void BackwardsTab(const VTInt numTabs) = 0; // CBT @@ -156,13 +157,9 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual void EndHyperlink() = 0; virtual void DoConEmuAction(const std::wstring_view string) = 0; - virtual void DoITerm2Action(const std::wstring_view string) = 0; - virtual void DoFinalTermAction(const std::wstring_view string) = 0; - virtual void DoVsCodeAction(const std::wstring_view string) = 0; - virtual void DoWTAction(const std::wstring_view string) = 0; virtual StringHandler DefineSixelImage(const VTInt macroParameter, diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index b210afa8215..5415bf3eb56 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2615,6 +2615,27 @@ void AdaptDispatch::SetWindowTitle(std::wstring_view title) _api.SetWindowTitle(title); } +// OSC 7 - Set Current Working Directory +// While ConEmu's OSC 9;9 works well for native Windows paths, +// OSC 7 uses file URIs, which may not always work. +void AdaptDispatch::SetCurrentWorkingDirectory(std::wstring_view uri) +{ + // Ensure that the URI has a null terminator. + std::wstring path{uri}; + + // PathCreateFromUrlW supports writing to the input pointer, + // and the resulting path can never be longer than the URI. + const auto ptr = path.data(); + auto len = gsl::narrow(path.size()); + THROW_IF_FAILED(PathCreateFromUrlW(ptr, ptr, &len, 0)); + path.resize(len); + + if (til::is_legal_path(path)) + { + _api.SetWorkingDirectory(path); + } +} + //Routine Description: // HTS - sets a VT tab stop in the cursor's current column. //Arguments: diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 9d89284813d..400362991b5 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -114,6 +114,7 @@ namespace Microsoft::Console::VirtualTerminal void BackIndex() override; // DECBI void ForwardIndex() override; // DECFI void SetWindowTitle(const std::wstring_view title) override; // DECSWT, OSCWindowTitle + void SetCurrentWorkingDirectory(std::wstring_view uri) override; // OSC 7 void HorizontalTabSet() override; // HTS void ForwardTab(const VTInt numTabs) override; // CHT, HT void BackwardsTab(const VTInt numTabs) override; // CBT diff --git a/src/terminal/adapter/precomp.h b/src/terminal/adapter/precomp.h index abed7d0b9f6..15b0adb896a 100644 --- a/src/terminal/adapter/precomp.h +++ b/src/terminal/adapter/precomp.h @@ -13,7 +13,8 @@ Module Name: // This includes support libraries from the CRT, STL, WIL, and GSL #include "LibraryIncludes.h" -#include +#include + #define ENABLE_INTSAFE_SIGNED_FUNCTIONS #include diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 7f154f66870..5949c701cec 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -69,6 +69,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons void BackIndex() override {} // DECBI void ForwardIndex() override {} // DECFI void SetWindowTitle(std::wstring_view /*title*/) override {} // DECSWT, OscWindowTitle + void SetCurrentWorkingDirectory(std::wstring_view /*uri*/) override {} // OSC 7 void HorizontalTabSet() override {} // HTS void ForwardTab(const VTInt /*numTabs*/) override {} // CHT, HT void BackwardsTab(const VTInt /*numTabs*/) override {} // CBT diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 59a88002045..1ccb03b5393 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -865,8 +865,7 @@ bool OutputStateMachineEngine::ActionOscDispatch(const size_t parameter, const s break; } case OscActionCodes::CurrentWorkingDirectory: - // TODO: Add support for OSC 7 = CWD sequences? - // In GH#8214 it was decided that it's a bad idea due to WSL compat. + _dispatch->SetCurrentWorkingDirectory(string); break; case OscActionCodes::Hyperlink: { diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp index b06f1712064..a86e78f372b 100644 --- a/src/types/inc/utils.hpp +++ b/src/types/inc/utils.hpp @@ -128,6 +128,8 @@ namespace Microsoft::Console::Utils const wchar_t* FindActionableControlCharacter(const wchar_t* beg, const size_t len) noexcept; + bool IsValidDirectory(const wchar_t* path) noexcept; + // Same deal, but in TerminalPage::_evaluatePathForCwd std::wstring EvaluateStartingDirectory(std::wstring_view cwd, std::wstring_view startingDirectory); diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 1044b17abdf..65423467386 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -1237,6 +1237,19 @@ const wchar_t* Utils::FindActionableControlCharacter(const wchar_t* beg, const s return it; } +// Returns `path` if it's a valid directory. Otherwise, returns an empty string. +bool Utils::IsValidDirectory(const wchar_t* path) noexcept +{ + if (path == nullptr || *path == L'\0') + { + return false; + } + + WIN32_FILE_ATTRIBUTE_DATA data; + const auto ok = GetFileAttributesExW(path, GetFileExInfoStandard, &data); + return ok && (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +} + #pragma warning(pop) std::wstring Utils::EvaluateStartingDirectory( From 1a3c2a4a508624b3fb94b4d8d759a50e1537114b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 30 Mar 2026 18:30:15 +0200 Subject: [PATCH 2/3] Address feedback --- src/common.build.pre.props | 2 +- src/types/utils.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common.build.pre.props b/src/common.build.pre.props index e8084905fcc..b3f57bf2302 100644 --- a/src/common.build.pre.props +++ b/src/common.build.pre.props @@ -95,7 +95,7 @@ - v145 + v143 Unicode false x64 diff --git a/src/types/utils.cpp b/src/types/utils.cpp index 65423467386..18af447d8bf 100644 --- a/src/types/utils.cpp +++ b/src/types/utils.cpp @@ -1237,7 +1237,7 @@ const wchar_t* Utils::FindActionableControlCharacter(const wchar_t* beg, const s return it; } -// Returns `path` if it's a valid directory. Otherwise, returns an empty string. +// Returns true if it's a valid path to a directory. bool Utils::IsValidDirectory(const wchar_t* path) noexcept { if (path == nullptr || *path == L'\0') From bd5116acfd73af6cb9ef421f8dbfcf31cae32d59 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 30 Mar 2026 19:35:22 +0200 Subject: [PATCH 3/3] Format --- src/terminal/adapter/adaptDispatch.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 5415bf3eb56..22cbf95e2bd 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2621,8 +2621,8 @@ void AdaptDispatch::SetWindowTitle(std::wstring_view title) void AdaptDispatch::SetCurrentWorkingDirectory(std::wstring_view uri) { // Ensure that the URI has a null terminator. - std::wstring path{uri}; - + std::wstring path{ uri }; + // PathCreateFromUrlW supports writing to the input pointer, // and the resulting path can never be longer than the URI. const auto ptr = path.data();