Repository: microsoft/terminal (language: cpp)

Issue:
Add an action to "Open in File Explorer"

Kinda the opposite of the context menu. Open the CWD in file explorer. Trick here, shell needs to have been configured for path integration.

Maybe there's a way to make the action do nothing if the active Terminal hasn't set one?

Also relevant: #3337, #5916


Other thoughts: What if the user has a path selected? Check if that text is a path, that path exists, and if so, then open _that_ path in explorer maybe?


Discussion / hints:
It seems like this ought to be a feature of the shell.  `START .` works in CMD, PowerShell, and TCC.  In TCC I can assign it to a keystroke.

> shell needs to have been configured for path integration

What does that mean?  Does it refer to some mechanism that already exists?
I mean, theoretically, yea. This is super doable in the shell. I'm more tracking this as a sub-task of the right-click context menu, or as a trigger. We need an action internally to hook up the context menu entry.

> What does that mean? Does it refer to some mechanism that already exists

Yea I'm talking about the `OSC9;9` shell integration, as described in https://docs.microsoft.com/en-us/windows/terminal/tutorials/new-tab-same-directory
> Yea I'm talking about the OSC9;9 shell integration

That's pretty neat and works as advertized (along with Ctrl+Shift+d).  It's the sort of thing I don't often want to do.  Most likely, by the next time I want to duplicate a tab, I'll have forgotten about the keystroke (and this thread).

Are there other benefits that could come from a shell using OSC9;9 to send a string to WT?

Is OSC9;9 documented anywhere other then the tutorial you mentioned?  I didn't find it in https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences.
> Are there other benefits that could come from a shell using OSC9;9 to send a string to WT?

At the moment not really, but who knows what we might do with it in the future. Like, maybe #12863. 

OSC9;9 was originally authored by ConEmu so they've got slightly better docs https://conemu.github.io/en/ShellWorkDir.html
> Like, maybe https://github.com/microsoft/terminal/issues/12863

That I already have in TCC (popup history and dirhistory).  New TCCs use GUI windows for this by default and that doesn't work very well with WT because TCC has no clue where the command line interface window _IS_.  So I use the older option to create the popups in the console screen buffer.

I'm aware of another thread that deals with console apps creating windows.  I'm in favor of any improvements on that front, in particular, getting such "popup" windows to have a position which is based on the position of WT's tab.  As it is, can WT convey such information to conhost and, if so, can a console app get it?
>in particular, getting such "popup" windows to have a position which is based on the position of WT's tab

I'm interested in this as well. We can get the WT position with `GetWindowRect` (`winuser.h`) on `TerminalPage::_hostingHwnd`, then it's just a question of starting the explorer window in a secure way. 

Any suggestions? I'm considering `CreateProcess` from `processthreadsapi.h`.

Here's what a potential handler in `AppActionHandlers` could look like (albeit untested), I used the current implementation of #8330 's code for retrieving the CWD:

```c++
void TerminalPage::_HandleOpenInFileExplorer(const IInspectable& /* sender */,
                                             const ActionEventArgs& actionArgs)
{
    const auto focusedTab{ _GetFocusedTabImpl() };
    if (focusedTab)
    {
        const auto workingDirectory = focusedTab->GetActiveTerminalControl().WorkingDirectory();
        const auto validWorkingDirectory = !workingDirectory.empty();
        if (validWorkingDirectory && _hostingHwnd.has_value())
        {
            LPRECT lpRect;
            GetWindowRect(TerminalPage::_hostingHwnd.value(), lpRect);

            // Spawn Explorer
        }
    }
}
```

Quick update: spawning Explorer with `ShellExecuteW` is trivial. However, obtaining the `HWND` to call `MoveWindow` is not. I'm open to suggestions on how to proceed, I really don't want to do the established solution of "wait for new process to idle -> enumerate windows -> get window by pid" because that little stutter (which could be a big stutter on weaker systems) followed by an enumeration drops performance shockingly quickly. 

I'm also lost on trying to add my handler to the Command Palette. It's registered as an action in the X-macros and I've been testing it through a Command in `defaults-universal.json` as well as a tab flyout context menu item which looks quite nifty: 
![image](https://user-images.githubusercontent.com/59982409/164954539-9df8b130-b159-4214-9168-257c8f642ec4.png)
However, I do think it needs to be in Command Palette for ease of use. Could someone please point me in the right direction? Thanks!
> However, obtaining the `HWND` to call `MoveWindow` is not

Wait hold up why do we need to do that at all? I think it's perfectly fine to just `ShellExecute({the path})` and let explorer just do it's thing. At least for a first version of the feature, yea?



> a Command in `defaults-universal.json`

Whoops, we should probably just delete that file - it's not actually used anymore 😅 `defaults.json` is the file you're looking for, that one should have many, MANY more actions. 
>we should probably just delete that file - it's not actually used anymore 😅 `defaults.json` is the file you're looking for

Hmm. I definitely put it in `defaults.json` as well. Here's what I used:

`{ "command": "openInFileExplorer", "keys": "ctrl+shift+alt+e" },`

The keyboard shortcut works, but it still doesn't show up in command palette. Do I have to add it to another place?

Side note, someone should really make a tutorial for registering an action to command palette. I'll open an issue for it when I finish figuring this one out. Thanks!
> Do I have to add it to another place?

Ah, probably. There's a pile of places you need to add this stuff, though, fewer than there used to be. I bet you're missing an entry in [`ActionAndArgs::GenerateName`](https://github.com/microsoft/terminal/blob/main/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp#L317). That's what generates the names for actions in the command palette. 

> Side note, someone should really make a tutorial for registering an action to command palette

You know, there actually is a guide, but it's pretty out of date: https://github.com/microsoft/terminal/blob/main/doc/cascadia/AddASetting.md#adding-an-action. We've since added some x-macros to the project that take care of a LOT of the boilerplate, but clearly not all of it. 

I might have you also refer to #12097 - looks like that was the most recent merged PR to add an action. That one also needed action args, which I don't think we need in this case, so that should simplify things a bit. 

If I'm understanding the OP correctly, then the following command will open a file manager at the current CLI working directory:

    explorer .

that is; launch `explorer` with the first argument matching (expanded to) the current path (`.` = current directory, compare `..` = parent directory).

Been using this for years (decades?), to great effect.

I'm not sure what the equivalent for PowerShell syntax is.
There's an actual shortcut which is ii (invoke-item).
What about instead of having a hidden away menu button, we detect file paths and turning them into hyperlinks?
![image](https://github.com/user-attachments/assets/d3540c12-af7d-4b9a-9a11-5c83ecc40972)
![image](https://github.com/user-attachments/assets/04b02770-80d7-4fcf-9f67-f04f3e006edb)
![image](https://github.com/user-attachments/assets/ef7e8519-b4a9-4bab-967a-51912116b466)

It would then also work with not just the current working directory, but any path from any output.
Lets say you run a compiler and it spits out a log containing multiple paths, it would be handy to be able to ctrl+click on them to go right to the URI:
```
Microsoft Windows [Version 10.0.19045.4717]
(c) Microsoft Corporation. All rights reserved.

C:\Users\Kieran Devlin> compiler.exe run
Compiling 28 source files...
Binary generated in C:\Users\Kieran Devlin\sources\app1\dist\bin\my-bin.exe
Full output log: C:\Users\Kieran Devlin\sources\app1\logs\compiler\2024-08-16 12-01-56.txt
```



Fix patch:
diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json
index c0b659ccc42..67c5e7aaae2 100644
--- a/doc/cascadia/profiles.schema.json
+++ b/doc/cascadia/profiles.schema.json
@@ -423,6 +423,7 @@
         "newWindow",
         "nextTab",
         "openAbout",
+        "openCWD",
         "openNewTabDropdown",
         "openSettings",
         "openSystemMenu",
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 8675fb98bac..9ff70e2a456 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -1128,6 +1128,16 @@ namespace winrt::TerminalApp::implementation
         }
     }
 
+    void TerminalPage::_HandleOpenCWD(const IInspectable& /*sender*/,
+                                      const ActionEventArgs& args)
+    {
+        if (const auto& control{ _GetActiveControl() })
+        {
+            control.OpenCWD();
+            args.Handled(true);
+        }
+    }
+
     void TerminalPage::_HandleGlobalSummon(const IInspectable& /*sender*/,
                                            const ActionEventArgs& args)
     {
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 91db886cebb..df59f8051eb 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -1643,6 +1643,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         SearchMissingCommand.raise(*this, make<implementation::SearchMissingCommandEventArgs>(hstring{ missingCommand }, bufferRow));
     }
 
+    void ControlCore::OpenCWD()
+    {
+        const auto workingDirectory = WorkingDirectory();
+        ShellExecute(nullptr, nullptr, L"explorer", workingDirectory.c_str(), nullptr, SW_SHOW);
+    }
+
     void ControlCore::ClearQuickFix()
     {
         _cachedQuickFixes = nullptr;
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index 2e9f2490d9d..80d4cf0ddd8 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -157,6 +157,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         void ClearQuickFix();
 
+        void OpenCWD();
+
 #pragma region ICoreState
         const size_t TaskbarState() const noexcept;
         const size_t TaskbarProgress() const noexcept;
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index ee1a4787e22..befc3c089f1 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -177,6 +177,8 @@ namespace Microsoft.Terminal.Control
         Boolean ShouldShowSelectCommand();
         Boolean ShouldShowSelectOutput();
 
+        void OpenCWD();
+
         void ClearQuickFix();
 
         // These events are called from some background thread
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index f76c8c59403..83f59d6a800 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -2615,6 +2615,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
     }
 
+    void TermControl::OpenCWD()
+    {
+        _core.OpenCWD();
+    }
+
     void TermControl::Close()
     {
         if (!_IsClosing())
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index d8a44161dbf..2c7e736ca22 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -69,6 +69,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         bool ExpandSelectionToWord();
         void RestoreFromPath(winrt::hstring path);
         void PersistToPath(const winrt::hstring& path) const;
+        void OpenCWD();
         void Close();
         Windows::Foundation::Size CharacterDimensions() const;
         Windows::Foundation::Size MinimumSize();
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index c60b5884540..da63b8b6b06 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -97,6 +97,7 @@ namespace Microsoft.Terminal.Control
         void ClearBuffer(ClearBufferType clearType);
         void RestoreFromPath(String path);
         void PersistToPath(String path);
+        void OpenCWD();
         void Close();
         Windows.Foundation.Size CharacterDimensions { get; };
         Windows.Foundation.Size MinimumSize { get; };
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 5d2bd486266..b8ccf1c761b 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -100,6 +100,7 @@ static constexpr std::string_view ToggleBroadcastInputKey{ "toggleBroadcastInput
 static constexpr std::string_view OpenScratchpadKey{ "experimental.openScratchpad" };
 static constexpr std::string_view OpenAboutKey{ "openAbout" };
 static constexpr std::string_view QuickFixKey{ "quickFix" };
+static constexpr std::string_view OpenCWDKey{ "openCWD" };
 
 static constexpr std::string_view ActionKey{ "action" };
 
@@ -437,6 +438,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::OpenScratchpad, RS_(L"OpenScratchpadKey") },
                 { ShortcutAction::OpenAbout, RS_(L"OpenAboutCommandKey") },
                 { ShortcutAction::QuickFix, RS_(L"QuickFixCommandKey") },
+                { ShortcutAction::OpenCWD, RS_(L"OpenCWDCommandKey") },
             };
         }();
 
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index daeb1f399bd..b9855d9e308 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -112,7 +112,8 @@
     ON_ALL_ACTIONS(ToggleBroadcastInput)    \
     ON_ALL_ACTIONS(OpenScratchpad)          \
     ON_ALL_ACTIONS(OpenAbout)               \
-    ON_ALL_ACTIONS(QuickFix)
+    ON_ALL_ACTIONS(QuickFix)                \
+    ON_ALL_ACTIONS(OpenCWD)
 
 #define ALL_SHORTCUT_ACTIONS_WITH_ARGS             \
     ON_ALL_ACTIONS_WITH_ARGS(AdjustFontSize)       \
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index 77c98c7da0a..eda7808fc81 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -734,4 +734,7 @@
   <data name="QuickFixCommandKey" xml:space="preserve">
     <value>Open quick fix menu</value>
   </data>
+  <data name="OpenCWDCommandKey" xml:space="preserve">
+    <value>Open current working directory</value>
+  </data>
 </root>
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 1d2b8f2c472..e64e676b5e6 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -449,6 +449,7 @@
         { "command": "experimental.openTasks", "id": "Terminal.OpenTasks" },
         { "command": "quickFix", "id": "Terminal.QuickFix" },
         { "command": { "action": "showSuggestions", "source": "all"}, "id": "Terminal.Suggestions" },
+        { "command": "openCWD", "id": "Terminal.OpenCWD" },
 
         // Tab Management
         // "command": "closeTab" is unbound by default.


Test patch: