Repository: microsoft/terminal (language: cpp)

Issue:
Terminal + Neovim: FocusLost/Gained events don't fire.
### Windows Terminal version (or Windows build number)

1.12.2931.0

### Other Software

Neovim 0.5+

### Steps to reproduce

Steps to reproduce:
1) Run Neovim 0.5 or higher using the following `min.vim` file, with the command `nvim -u min.vim --clean`
```vimL
" min.vim
autocmd FocusLost * echo "Focus Lost"
autocmd FocusGained * echo "Focus Gained"
```
2) Click away from terminal. Click to.
3) Vim command line output at the bottom of the screen should update with the echoed text.

### Expected Behavior

See title and steps to reproduce. This works in all Linux/MacOS terminals I've used, including Alacritty, Kitty, and iTerm2, as well as in tmux sessions if focus-events are enabled.

The Vim/Neovim release is not particularly important unless you would also like to test in tmux; in that case, choose a release any time after April 2021 and put `set -g focus-events on` in your `.tmux.conf`

### Actual Behavior

Nothing happens.


Discussion / hints:
I suspect this is because we don't support XTerm's focus event mode (private mode 1004). I thought there might have been an issue for it already, but I haven't been able find one.

Edit: Not a duplicate as such, but I see now it has been discussed before here: https://github.com/microsoft/terminal/issues/10531#issuecomment-872398721
Well cool, I would have suspected there was an issue already for this. Plumbing through conpty shouldn't be too hard, if a bit annoying. 

Fix patch:
diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp
index c2618b0bc7f..811831edead 100644
--- a/src/cascadia/TerminalControl/ControlCore.cpp
+++ b/src/cascadia/TerminalControl/ControlCore.cpp
@@ -1708,13 +1708,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation
     // - This is related to work done for GH#2988.
     void ControlCore::GotFocus()
     {
-        _connection.WriteInput(L"\x1b[I");
+        _terminal->FocusChanged(true);
     }
 
     // See GotFocus.
     void ControlCore::LostFocus()
     {
-        _connection.WriteInput(L"\x1b[O");
+        _terminal->FocusChanged(false);
     }
 
     bool ControlCore::_isBackgroundTransparent()
diff --git a/src/cascadia/TerminalCore/ITerminalInput.hpp b/src/cascadia/TerminalCore/ITerminalInput.hpp
index c2c7c54ca1b..1db443f51d6 100644
--- a/src/cascadia/TerminalCore/ITerminalInput.hpp
+++ b/src/cascadia/TerminalCore/ITerminalInput.hpp
@@ -26,6 +26,8 @@ namespace Microsoft::Terminal::Core
 
         virtual void TrySnapOnInput() = 0;
 
+        virtual void FocusChanged(const bool focused) = 0;
+
     protected:
         ITerminalInput() = default;
     };
diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp
index 90928513fce..197c83d563d 100644
--- a/src/cascadia/TerminalCore/Terminal.cpp
+++ b/src/cascadia/TerminalCore/Terminal.cpp
@@ -746,6 +746,19 @@ bool Terminal::SendCharEvent(const wchar_t ch, const WORD scanCode, const Contro
     return handledDown || handledUp;
 }
 
+// Method Description:
+// - Tell the terminal input that we gained or lost focus. If the client
+//   requested focus events, this will send a message to them.
+// - ConPTY ALWAYS wants focus events.
+// Arguments:
+// - focused: true if we're focused, false otherwise.
+// Return Value:
+// - none
+void Terminal::FocusChanged(const bool focused) noexcept
+{
+    _terminalInput->HandleFocus(focused);
+}
+
 // Method Description:
 // - Invalidates the regions described in the given pattern tree for the rendering purposes
 // Arguments:
diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp
index df882f3eed0..c9b8851e368 100644
--- a/src/cascadia/TerminalCore/Terminal.hpp
+++ b/src/cascadia/TerminalCore/Terminal.hpp
@@ -156,6 +156,8 @@ class Microsoft::Terminal::Core::Terminal final :
     bool IsTrackingMouseInput() const noexcept;
     bool ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const noexcept;
 
+    void FocusChanged(const bool focused) noexcept override;
+
     std::wstring GetHyperlinkAtPosition(const COORD position);
     uint16_t GetHyperlinkIdAtPosition(const COORD position);
     std::optional<interval_tree::IntervalTree<til::point, size_t>::interval> GetHyperlinkIntervalFromPosition(const COORD position);
diff --git a/src/cascadia/TerminalCore/TerminalDispatch.cpp b/src/cascadia/TerminalCore/TerminalDispatch.cpp
index 689e29de93a..ad41aa85d46 100644
--- a/src/cascadia/TerminalCore/TerminalDispatch.cpp
+++ b/src/cascadia/TerminalCore/TerminalDispatch.cpp
@@ -395,7 +395,6 @@ bool TerminalDispatch::EnableButtonEventMouseMode(const bool enabled)
 
 //Routine Description:
 // Enable Any Event mode - send all mouse events to the input.
-
 //Arguments:
 // - enabled - true to enable, false to disable.
 // Return value:
@@ -406,6 +405,20 @@ bool TerminalDispatch::EnableAnyEventMouseMode(const bool enabled)
     return true;
 }
 
+// Method Description:
+// - Enables/disables focus event mode. A client may enable this if they want to
+//   receive focus events.
+// - ConPTY always enables this mode and never disables it. For more details, see GH#12900.
+// Arguments:
+// - enabled - true to enable, false to disable.
+// Return Value:
+// - True if handled successfully. False otherwise.
+bool TerminalDispatch::EnableFocusEventMode(const bool enabled)
+{
+    _terminalApi.SetInputMode(TerminalInput::Mode::FocusEvent, enabled);
+    return true;
+}
+
 //Routine Description:
 // Enable Alternate Scroll Mode - When in the Alt Buffer, send CUP and CUD on
 //      scroll up/down events instead of the usual sequences
@@ -643,6 +656,9 @@ bool TerminalDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param,
     case DispatchTypes::ModeParams::SGR_EXTENDED_MODE:
         success = EnableSGRExtendedMouseMode(enable);
         break;
+    case DispatchTypes::ModeParams::FOCUS_EVENT_MODE:
+        success = EnableFocusEventMode(enable);
+        break;
     case DispatchTypes::ModeParams::ALTERNATE_SCROLL:
         success = EnableAlternateScroll(enable);
         break;
diff --git a/src/cascadia/TerminalCore/TerminalDispatch.hpp b/src/cascadia/TerminalCore/TerminalDispatch.hpp
index 42188bc4ecf..b83b90eb2e0 100644
--- a/src/cascadia/TerminalCore/TerminalDispatch.hpp
+++ b/src/cascadia/TerminalCore/TerminalDispatch.hpp
@@ -72,6 +72,7 @@ class TerminalDispatch : public Microsoft::Console::VirtualTerminal::TermDispatc
     bool EnableSGRExtendedMouseMode(const bool enabled) override; // ?1006
     bool EnableButtonEventMouseMode(const bool enabled) override; // ?1002
     bool EnableAnyEventMouseMode(const bool enabled) override; // ?1003
+    bool EnableFocusEventMode(const bool enabled) override; // ?1004
     bool EnableAlternateScroll(const bool enabled) override; // ?1007
     bool EnableXtermBracketedPasteMode(const bool enabled) override; // ?2004
 
diff --git a/src/host/inputBuffer.cpp b/src/host/inputBuffer.cpp
index 8283ef213e1..b2a00fdba5a 100644
--- a/src/host/inputBuffer.cpp
+++ b/src/host/inputBuffer.cpp
@@ -642,6 +642,7 @@ void InputBuffer::_WriteBuffer(_Inout_ std::deque<std::unique_ptr<IInputEvent>>&
         inEvents.pop_front();
         if (vtInputMode)
         {
+            // GH#11682: TerminalInput::HandleKey can handle both KeyEvents and Focus events seamlessly
             const bool handled = _termInput.HandleKey(inEvent.get());
             if (handled)
             {
diff --git a/src/renderer/vt/VtSequences.cpp b/src/renderer/vt/VtSequences.cpp
index 05fd6f95184..56939fb175e 100644
--- a/src/renderer/vt/VtSequences.cpp
+++ b/src/renderer/vt/VtSequences.cpp
@@ -434,6 +434,11 @@ using namespace Microsoft::Console::Render;
     return _Write("\x1b[?9001h");
 }
 
+[[nodiscard]] HRESULT VtEngine::_RequestFocusEventMode() noexcept
+{
+    return _Write("\x1b[?1004h");
+}
+
 // Method Description:
 // - Send a sequence to the connected terminal to switch to the alternate or main screen buffer.
 // Arguments:
diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp
index 1f3cf516461..3669a0440eb 100644
--- a/src/renderer/vt/state.cpp
+++ b/src/renderer/vt/state.cpp
@@ -524,6 +524,7 @@ void VtEngine::SetTerminalCursorTextPosition(const COORD cursor) noexcept
 HRESULT VtEngine::RequestWin32Input() noexcept
 {
     RETURN_IF_FAILED(_RequestWin32Input());
+    RETURN_IF_FAILED(_RequestFocusEventMode());
     RETURN_IF_FAILED(_Flush());
     return S_OK;
 }
diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp
index cd8a3d5d739..cd1d44fff46 100644
--- a/src/renderer/vt/vtrenderer.hpp
+++ b/src/renderer/vt/vtrenderer.hpp
@@ -200,6 +200,8 @@ namespace Microsoft::Console::Render
         [[nodiscard]] HRESULT _RequestWin32Input() noexcept;
         [[nodiscard]] HRESULT _SwitchScreenBuffer(const bool useAltBuffer) noexcept;
 
+        [[nodiscard]] HRESULT _RequestFocusEventMode() noexcept;
+
         [[nodiscard]] virtual HRESULT _MoveCursor(const COORD coord) noexcept = 0;
         [[nodiscard]] HRESULT _RgbUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept;
         [[nodiscard]] HRESULT _16ColorUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept;
diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp
index 4510626f50b..1d9ae95b278 100644
--- a/src/terminal/adapter/DispatchTypes.hpp
+++ b/src/terminal/adapter/DispatchTypes.hpp
@@ -377,6 +377,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
         VT200_MOUSE_MODE = DECPrivateMode(1000),
         BUTTON_EVENT_MOUSE_MODE = DECPrivateMode(1002),
         ANY_EVENT_MOUSE_MODE = DECPrivateMode(1003),
+        FOCUS_EVENT_MODE = DECPrivateMode(1004),
         UTF8_EXTENDED_MODE = DECPrivateMode(1005),
         SGR_EXTENDED_MODE = DECPrivateMode(1006),
         ALTERNATE_SCROLL = DECPrivateMode(1007),
diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp
index f0f48fa2d2f..076bd1ea587 100644
--- a/src/terminal/adapter/ITermDispatch.hpp
+++ b/src/terminal/adapter/ITermDispatch.hpp
@@ -79,6 +79,7 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
     virtual bool EnableSGRExtendedMouseMode(const bool enabled) = 0; // ?1006
     virtual bool EnableButtonEventMouseMode(const bool enabled) = 0; // ?1002
     virtual bool EnableAnyEventMouseMode(const bool enabled) = 0; // ?1003
+    virtual bool EnableFocusEventMode(const bool enabled) = 0; // ?1004
     virtual bool EnableAlternateScroll(const bool enabled) = 0; // ?1007
     virtual bool EnableXtermBracketedPasteMode(const bool enabled) = 0; // ?2004
     virtual bool SetColorTableEntry(const size_t tableIndex, const DWORD color) = 0; // OSCColorTable
diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp
index 5294b74292f..a2e9cc9fc75 100644
--- a/src/terminal/adapter/InteractDispatch.cpp
+++ b/src/terminal/adapter/InteractDispatch.cpp
@@ -188,7 +188,6 @@ bool InteractDispatch::IsVtInputEnabled() const
 //   which will end up here. This will update the console's internal tracker if
 //   it's focused or not, as to match the end-terminal's state.
 // - Used to call ConsoleControl(ConsoleSetForeground,...).
-// - Full support for this sequence is tracked in GH#11682.
 // Arguments:
 // - focused: if the terminal is now focused
 // Return Value:
@@ -247,11 +246,9 @@ bool InteractDispatch::FocusChanged(const bool focused) const
 
         WI_UpdateFlag(gci.Flags, CONSOLE_HAS_FOCUS, shouldActuallyFocus);
         gci.ProcessHandleList.ModifyConsoleProcessFocus(shouldActuallyFocus);
+        gci.pInputBuffer->Write(std::make_unique<FocusEvent>(focused));
     }
     // Does nothing outside of ConPTY. If there's a real HWND, then the HWND is solely in charge.
 
-    // Theoretically, this could be propagated as a focus event as well, to the
-    // input buffer. That should be considered when implementing GH#11682.
-
     return true;
 }
diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp
index 53b54f3b5ee..1ed4c9a3fdc 100644
--- a/src/terminal/adapter/adaptDispatch.cpp
+++ b/src/terminal/adapter/adaptDispatch.cpp
@@ -1025,7 +1025,24 @@ bool AdaptDispatch::_SetInputMode(const TerminalInput::Mode mode, const bool ena
     // us that SSH 7.7 _also_ requests mouse input and that can have a user interface
     // impact on the actual connected terminal. We can't remove this check,
     // because SSH <=7.7 is out in the wild on all versions of Windows <=2004.
-    return !(_pConApi->IsConsolePty() && _pConApi->IsVtInputEnabled());
+
+    // GH#12799 - If the app requested that we disable focus events, DON'T pass
+    // that through. ConPTY would _always_ like to know about focus events.
+
+    return !_pConApi->IsConsolePty() ||
+           !_pConApi->IsVtInputEnabled() ||
+           (!enable && mode == TerminalInput::Mode::FocusEvent);
+
+    // Another way of writing the above statement is:
+    //
+    // const bool inConpty = _pConApi->IsConsolePty();
+    // const bool shouldPassthrough = inConpty && _pConApi->IsVtInputEnabled();
+    // const bool disabledFocusEvents = inConpty && (!enable && mode == TerminalInput::Mode::FocusEvent);
+    // return !shouldPassthrough || disabledFocusEvents;
+    //
+    // It's like a "filter" left to right. Due to the early return via
+    // !IsConsolePty, once you're at the !enable part, IsConsolePty can only be
+    // true anymore.
 }
 
 // Routine Description:
@@ -1084,6 +1101,9 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con
     case DispatchTypes::ModeParams::SGR_EXTENDED_MODE:
         success = EnableSGRExtendedMouseMode(enable);
         break;
+    case DispatchTypes::ModeParams::FOCUS_EVENT_MODE:
+        success = EnableFocusEventMode(enable);
+        break;
     case DispatchTypes::ModeParams::ALTERNATE_SCROLL:
         success = EnableAlternateScroll(enable);
         break;
@@ -2091,7 +2111,6 @@ bool AdaptDispatch::EnableButtonEventMouseMode(const bool enabled)
 
 //Routine Description:
 // Enable Any Event mode - send all mouse events to the input.
-
 //Arguments:
 // - enabled - true to enable, false to disable.
 // Return value:
@@ -2101,6 +2120,21 @@ bool AdaptDispatch::EnableAnyEventMouseMode(const bool enabled)
     return _SetInputMode(TerminalInput::Mode::AnyEventMouseTracking, enabled);
 }
 
+// Method Description:
+// - Enables/disables focus event mode. A client may enable this if they want to
+//   receive focus events.
+// - ConPTY always enables this mode and never disables it. Internally, we'll
+//   always set this mode, but conpty will never request this to be disabled by
+//   the hosting terminal.
+// Arguments:
+// - enabled - true to enable, false to disable.
+// Return Value:
+// - True if handled successfully. False otherwise.
+bool AdaptDispatch::EnableFocusEventMode(const bool enabled)
+{
+    return _SetInputMode(TerminalInput::Mode::FocusEvent, enabled);
+}
+
 //Routine Description:
 // Enable Alternate Scroll Mode - When in the Alt Buffer, send CUP and CUD on
 //      scroll up/down events instead of the usual sequences
diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp
index 89bc2eb3647..70a35f2cdf2 100644
--- a/src/terminal/adapter/adaptDispatch.hpp
+++ b/src/terminal/adapter/adaptDispatch.hpp
@@ -107,6 +107,7 @@ namespace Microsoft::Console::VirtualTerminal
         bool EnableSGRExtendedMouseMode(const bool enabled) override; // ?1006
         bool EnableButtonEventMouseMode(const bool enabled) override; // ?1002
         bool EnableAnyEventMouseMode(const bool enabled) override; // ?1003
+        bool EnableFocusEventMode(const bool enabled) override; // ?1004
         bool EnableAlternateScroll(const bool enabled) override; // ?1007
         bool EnableXtermBracketedPasteMode(const bool enabled) noexcept override; // ?2004
         bool SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) override; // DECSCUSR
diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp
index 0d75258ed30..762d555dd4b 100644
--- a/src/terminal/adapter/termDispatch.hpp
+++ b/src/terminal/adapter/termDispatch.hpp
@@ -72,6 +72,7 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons
     bool EnableSGRExtendedMouseMode(const bool /*enabled*/) override { return false; } // ?1006
     bool EnableButtonEventMouseMode(const bool /*enabled*/) override { return false; } // ?1002
     bool EnableAnyEventMouseMode(const bool /*enabled*/) override { return false; } // ?1003
+    bool EnableFocusEventMode(const bool /*enabled*/) override { return false; } // ?1004
     bool EnableAlternateScroll(const bool /*enabled*/) override { return false; } // ?1007
     bool EnableXtermBracketedPasteMode(const bool /*enabled*/) override { return false; } // ?2004
     bool SetColorTableEntry(const size_t /*tableIndex*/, const DWORD /*color*/) override { return false; } // OSCColorTable
diff --git a/src/terminal/input/terminalInput.cpp b/src/terminal/input/terminalInput.cpp
index 1408ad56cbd..1210432c6d6 100644
--- a/src/terminal/input/terminalInput.cpp
+++ b/src/terminal/input/terminalInput.cpp
@@ -522,6 +522,14 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
         return false;
     }
 
+    // GH#11682: If this was a focus event, we can handle this. Steal the
+    // focused state, and return true if we're actually in focus event mode.
+    if (pInEvent->EventType() == InputEventType::FocusEvent)
+    {
+        const auto& focusEvent = *static_cast<const FocusEvent* const>(pInEvent);
+        return HandleFocus(focusEvent.GetFocus());
+    }
+
     // On key presses, prepare to translate to VT compatible sequences
     if (pInEvent->EventType() != InputEventType::KeyEvent)
     {
@@ -674,6 +682,16 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent)
     return false;
 }
 
+bool TerminalInput::HandleFocus(const bool focused) noexcept
+{
+    const bool enabled{ _inputMode.test(Mode::FocusEvent) };
+    if (enabled)
+    {
+        _SendInputSequence(focused ? L"\x1b[I" : L"\x1b[O");
+    }
+    return enabled;
+}
+
 // Routine Description:
 // - Sends the given character to the shell.
 // - Surrogate pairs are being aggregated by this function before being sent.
diff --git a/src/terminal/input/terminalInput.hpp b/src/terminal/input/terminalInput.hpp
index 6b18f5ebc90..280aea1fae7 100644
--- a/src/terminal/input/terminalInput.hpp
+++ b/src/terminal/input/terminalInput.hpp
@@ -34,6 +34,7 @@ namespace Microsoft::Console::VirtualTerminal
         ~TerminalInput() = default;
 
         bool HandleKey(const IInputEvent* const pInEvent);
+        bool HandleFocus(const bool focused) noexcept;
 
         enum class Mode : size_t
         {
@@ -49,6 +50,8 @@ namespace Microsoft::Console::VirtualTerminal
             ButtonEventMouseTracking,
             AnyEventMouseTracking,
 
+            FocusEvent,
+
             AlternateScroll
         };
 


Test patch: