Repository: microsoft/terminal (language: cpp)

Issue:
Add Enable/Disable actions alongside "Toggle pane read-only mode"
<!-- 
🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨

I ACKNOWLEDGE THE FOLLOWING BEFORE PROCEEDING:
1. If I delete this entire template and go my own path, the core team may close my issue without further explanation or engagement.
2. If I list multiple bugs/concerns in this one issue, the core team may close my issue without further explanation or engagement.
3. If I write an issue that has many duplicates, the core team may close my issue without further explanation or engagement (and without necessarily spending time to find the exact duplicate ID number).
4. If I leave the title incomplete when filing the issue, the core team may close my issue without further explanation or engagement.
5. If I file something completely blank in the body, the core team may close my issue without further explanation or engagement.

All good? Then proceed!
-->

# Description of the new feature/enhancement
Two additional terminal actions:
- `Enable pane read-only mode`
- `Disable pane read-only mode`

### Reasoning
<!-- 
A clear and concise description of what the problem is that the new feature would solve.
Describe why and how a user would use this new functionality (if applicable).
-->
I make use of the Read-Only mode occasionally, usually whenever I have multiple terminal windows open and a long-running command running in one which prevents me from accidentally hitting "Ctrl+C" in the unintended window. 

Read-Only mode has the nice feature of showing a padlock icon next to the pane's title whenever Read-Only mode is active, but of course this helpful hint is not visible if the terminal is in Full-Screen mode. 

Whenever that scenario (toggling read-only mode in a fullscreen window) comes up for me, I'll usually just hit `space`, which will show a pop-up if the pane is in Read-Only mode, and if no pop-up is shown, I'll know to run the action to toggle read-only mode.

I think it would be helpful to have the 2 actions I proposed above, neither of which should care whether Read-Only mode is already active or not, and then internally either perform the toggle of read-only mode, or noop. These actions would provide a more assured method of setting the intended mode for a pane and don't need to prompt the user.

# Proposed technical implementation details (optional)
<!-- 
A clear and concise description of what you want to happen.
-->
My current workaround is a custom terminal action (defined in my `settings.json`):
```json
"actions": [
    {
        "name": "Toggle pane read-only mode (w/check)",
        "keys": "alt+r",
        "command": {
            "action": "multipleActions",
            "actions": [
                { "action": "toggleReadOnlyMode" },
                { "action": "sendInput", "input": " " }
            ]
        }
    },
]
```

This gives action provides an indication of the Read-Only mode state of a pane by forcefully showing the "Read-only mode is enabled" pop-up after sending a `space`, and if nothing happens when I run the action, I know I need to run it again.

I couldn't find any mechanism of setting/retrieving the current state of a pane's read-only mode, so I wasn't able to figure out an alternative solution for myself (for instance, defining a PowerShell function to check/toggle the read-only mode state, which could maybe be used in a Terminal action)

Given that the "Toggle pane read-only mode" is a pretty stable feature, it may be pretty simple to add these additional actions within the source code here. I'll give it my best shot and report back but I wanted to see if anyone had an opinion/suggestion as well.

> Thanks to all the contributors for all the great work you do on this project!


Discussion / hints:
This is such a reasonable and well written proposal, I can't help but say "sure, why not". The code wouldn't be hard to write (just a lot of plumbing) nor maintain. 
@zadjii-msft I think I am confident to be able to contribute to this. Would be my first contribution so bare with while I figure things out!
Absolutely, go for it! Lemme know if you need any pointers. 

> **Note**
> ## Walkthrough

* #14549 is a great example of where to start - that's another PR that adds an action and plumbs it through the settings model and hooks up the implementation from `TerminalPage` -> `TermControl`
* Except, instead of "RestartConnection", it'd be like two actions `EnableReadOnlyMode` and `DisableReadOnlyMode`.

Fix patch:
diff --git a/src/cascadia/TerminalApp/AppActionHandlers.cpp b/src/cascadia/TerminalApp/AppActionHandlers.cpp
index 8b2cd7b3637..ae0e8429033 100644
--- a/src/cascadia/TerminalApp/AppActionHandlers.cpp
+++ b/src/cascadia/TerminalApp/AppActionHandlers.cpp
@@ -285,6 +285,28 @@ namespace winrt::TerminalApp::implementation
         args.Handled(true);
     }
 
+    void TerminalPage::_HandleEnablePaneReadOnly(const IInspectable& /*sender*/,
+                                                 const ActionEventArgs& args)
+    {
+        if (const auto activeTab{ _GetFocusedTabImpl() })
+        {
+            activeTab->SetPaneReadOnly(true);
+        }
+
+        args.Handled(true);
+    }
+
+    void TerminalPage::_HandleDisablePaneReadOnly(const IInspectable& /*sender*/,
+                                                  const ActionEventArgs& args)
+    {
+        if (const auto activeTab{ _GetFocusedTabImpl() })
+        {
+            activeTab->SetPaneReadOnly(false);
+        }
+
+        args.Handled(true);
+    }
+
     void TerminalPage::_HandleScrollUpPage(const IInspectable& /*sender*/,
                                            const ActionEventArgs& args)
     {
diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp
index 8cd4ae86acd..b6d961d2c5b 100644
--- a/src/cascadia/TerminalApp/TerminalTab.cpp
+++ b/src/cascadia/TerminalApp/TerminalTab.cpp
@@ -1563,14 +1563,14 @@ namespace winrt::TerminalApp::implementation
     {
         auto hasReadOnly = false;
         auto allReadOnly = true;
-        _activePane->WalkTree([&](auto p) {
+        _activePane->WalkTree([&](const auto& p) {
             if (const auto& control{ p->GetTerminalControl() })
             {
                 hasReadOnly |= control.ReadOnly();
                 allReadOnly &= control.ReadOnly();
             }
         });
-        _activePane->WalkTree([&](auto p) {
+        _activePane->WalkTree([&](const auto& p) {
             if (const auto& control{ p->GetTerminalControl() })
             {
                 // If all controls have the same read only state then just toggle
@@ -1587,6 +1587,38 @@ namespace winrt::TerminalApp::implementation
         });
     }
 
+    // Method Description:
+    // - Set read-only mode on the active pane
+    // - If a parent pane is selected, this will ensure that all children have
+    //   the same read-only status.
+    void TerminalTab::SetPaneReadOnly(const bool readOnlyState)
+    {
+        auto hasReadOnly = false;
+        auto allReadOnly = true;
+        _activePane->WalkTree([&](const auto& p) {
+            if (const auto& control{ p->GetTerminalControl() })
+            {
+                hasReadOnly |= control.ReadOnly();
+                allReadOnly &= control.ReadOnly();
+            }
+        });
+        _activePane->WalkTree([&](const auto& p) {
+            if (const auto& control{ p->GetTerminalControl() })
+            {
+                // If all controls have the same read only state then just disable
+                if (allReadOnly || !hasReadOnly)
+                {
+                    control.SetReadOnly(readOnlyState);
+                }
+                // otherwise set to all read only.
+                else if (!control.ReadOnly())
+                {
+                    control.SetReadOnly(readOnlyState);
+                }
+            }
+        });
+    }
+
     // Method Description:
     // - Calculates if the tab is read-only.
     // The tab is considered read-only if one of the panes is read-only.
diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h
index 53492095db1..f607563e188 100644
--- a/src/cascadia/TerminalApp/TerminalTab.h
+++ b/src/cascadia/TerminalApp/TerminalTab.h
@@ -87,6 +87,8 @@ namespace winrt::TerminalApp::implementation
         int GetLeafPaneCount() const noexcept;
 
         void TogglePaneReadOnly();
+        void SetPaneReadOnly(const bool readOnlyState);
+
         std::shared_ptr<Pane> GetActivePane() const;
         winrt::TerminalApp::TaskbarState GetCombinedTaskbarState() const;
 
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index 1e7829940d1..2f3d4e44a86 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -1671,6 +1671,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _isReadOnly = !_isReadOnly;
     }
 
+    void ControlCore::SetReadOnlyMode(const bool readOnlyState)
+    {
+        _isReadOnly = readOnlyState;
+    }
+
     void ControlCore::_raiseReadOnlyWarning()
     {
         auto noticeArgs = winrt::make<NoticeEventArgs>(NoticeLevel::Info, RS_(L"TermControlReadOnly"));
diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h
index f9c7792f6ae..10582981d34 100644
--- a/src/cascadia/TerminalControl/ControlCore.h
+++ b/src/cascadia/TerminalControl/ControlCore.h
@@ -193,6 +193,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         bool IsInReadOnlyMode() const;
         void ToggleReadOnlyMode();
+        void SetReadOnlyMode(const bool readOnlyState);
 
         hstring ReadEntireBuffer() const;
 
diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl
index a8df501469b..e62f4f35adc 100644
--- a/src/cascadia/TerminalControl/ControlCore.idl
+++ b/src/cascadia/TerminalControl/ControlCore.idl
@@ -111,6 +111,7 @@ namespace Microsoft.Terminal.Control
 
         void ToggleShaderEffects();
         void ToggleReadOnlyMode();
+        void SetReadOnlyMode(Boolean readOnlyState);
 
         Microsoft.Terminal.Core.Point CursorPosition { get; };
         void ResumeRendering();
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 40081c30b62..11114910660 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -2925,6 +2925,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _ReadOnlyChangedHandlers(*this, winrt::box_value(_core.IsInReadOnlyMode()));
     }
 
+    // Method Description:
+    // - Sets the read-only flag, raises event describing the value change
+    void TermControl::SetReadOnly(const bool readOnlyState)
+    {
+        _core.SetReadOnlyMode(readOnlyState);
+        _ReadOnlyChangedHandlers(*this, winrt::box_value(_core.IsInReadOnlyMode()));
+    }
+
     // Method Description:
     // - Handle a mouse exited event, specifically clearing last hovered cell
     // and removing selection from hyper link if exists
diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h
index e8f0bb8734d..2dc5377b679 100644
--- a/src/cascadia/TerminalControl/TermControl.h
+++ b/src/cascadia/TerminalControl/TermControl.h
@@ -124,6 +124,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
 
         bool ReadOnly() const noexcept;
         void ToggleReadOnly();
+        void SetReadOnly(const bool readOnlyState);
 
         static Control::MouseButtonState GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point);
         static unsigned int GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point);
diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl
index 3c9f794f42c..b2fd88d0bbb 100644
--- a/src/cascadia/TerminalControl/TermControl.idl
+++ b/src/cascadia/TerminalControl/TermControl.idl
@@ -89,6 +89,7 @@ namespace Microsoft.Terminal.Control
 
         Boolean ReadOnly { get; };
         void ToggleReadOnly();
+        void SetReadOnly(Boolean readOnlyState);
 
         String ReadEntireBuffer();
 
diff --git a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
index 12ca43475d2..e43320f994d 100644
--- a/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
+++ b/src/cascadia/TerminalSettingsModel/ActionAndArgs.cpp
@@ -65,6 +65,8 @@ static constexpr std::string_view MoveTabKey{ "moveTab" };
 static constexpr std::string_view BreakIntoDebuggerKey{ "breakIntoDebugger" };
 static constexpr std::string_view FindMatchKey{ "findMatch" };
 static constexpr std::string_view TogglePaneReadOnlyKey{ "toggleReadOnlyMode" };
+static constexpr std::string_view EnablePaneReadOnlyKey{ "enableReadOnlyMode" };
+static constexpr std::string_view DisablePaneReadOnlyKey{ "disableReadOnlyMode" };
 static constexpr std::string_view NewWindowKey{ "newWindow" };
 static constexpr std::string_view IdentifyWindowKey{ "identifyWindow" };
 static constexpr std::string_view IdentifyWindowsKey{ "identifyWindows" };
@@ -387,6 +389,8 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation
                 { ShortcutAction::BreakIntoDebugger, RS_(L"BreakIntoDebuggerCommandKey") },
                 { ShortcutAction::FindMatch, L"" }, // Intentionally omitted, must be generated by GenerateName
                 { ShortcutAction::TogglePaneReadOnly, RS_(L"TogglePaneReadOnlyCommandKey") },
+                { ShortcutAction::EnablePaneReadOnly, RS_(L"EnablePaneReadOnlyCommandKey") },
+                { ShortcutAction::DisablePaneReadOnly, RS_(L"DisablePaneReadOnlyCommandKey") },
                 { ShortcutAction::NewWindow, RS_(L"NewWindowCommandKey") },
                 { ShortcutAction::IdentifyWindow, RS_(L"IdentifyWindowCommandKey") },
                 { ShortcutAction::IdentifyWindows, RS_(L"IdentifyWindowsCommandKey") },
diff --git a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
index efb861667c1..326409a7c23 100644
--- a/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
+++ b/src/cascadia/TerminalSettingsModel/AllShortcutActions.h
@@ -77,6 +77,8 @@
     ON_ALL_ACTIONS(MoveTab)                 \
     ON_ALL_ACTIONS(BreakIntoDebugger)       \
     ON_ALL_ACTIONS(TogglePaneReadOnly)      \
+    ON_ALL_ACTIONS(EnablePaneReadOnly)      \
+    ON_ALL_ACTIONS(DisablePaneReadOnly)     \
     ON_ALL_ACTIONS(FindMatch)               \
     ON_ALL_ACTIONS(NewWindow)               \
     ON_ALL_ACTIONS(IdentifyWindow)          \
diff --git a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
index 09998b9093f..c79aea0a705 100644
--- a/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
+++ b/src/cascadia/TerminalSettingsModel/Resources/en-US/Resources.resw
@@ -472,6 +472,12 @@
   <data name="TogglePaneReadOnlyCommandKey" xml:space="preserve">
     <value>Toggle pane read-only mode</value>
   </data>
+  <data name="EnablePaneReadOnlyCommandKey" xml:space="preserve">
+    <value>Enable pane read-only mode</value>
+  </data>
+  <data name="DisablePaneReadOnlyCommandKey" xml:space="preserve">
+    <value>Disable pane read-only mode</value>
+  </data>
   <data name="ToggleShaderEffectsCommandKey" xml:space="preserve">
     <value>Toggle terminal visual effects</value>
   </data>
diff --git a/src/cascadia/TerminalSettingsModel/defaults.json b/src/cascadia/TerminalSettingsModel/defaults.json
index 59a7f231c81..9d850dd1c9b 100644
--- a/src/cascadia/TerminalSettingsModel/defaults.json
+++ b/src/cascadia/TerminalSettingsModel/defaults.json
@@ -408,6 +408,8 @@
         { "command": "togglePaneZoom" },
         { "command": "toggleSplitOrientation" },
         { "command": "toggleReadOnlyMode" },
+        { "command": "enableReadOnlyMode" },
+        { "command": "disableReadOnlyMode" },
         { "command": { "action": "movePane", "index": 0 } },
         { "command": { "action": "movePane", "index": 1 } },
         { "command": { "action": "movePane", "index": 2 } },


Test patch: