Repository: microsoft/terminal (language: cpp)

Issue:
[Terminal Canary] Cursor doesn't show up in Terminal
### Windows Terminal version

1.23.10011.0

### Windows build number

10.0.27764.1000

### Other Software

sudo for Windows (1.0.1)

### Steps to reproduce

1. Launch Terminal Canary 
2. Launch a command with `sudo`
3. Hover over the terminal with the cursor

### Expected Behavior

The cursor should be visible when you hover over the Terminal launched with administrative privileges.

### Actual Behavior

Cursor isn't showing when you hover over the Terminal.

https://github.com/user-attachments/assets/b0ace3d3-c69e-475b-bac9-c864db5d76c7




Discussion / hints:
Thanks for the report! This was most likely fixed by #18345. It took a while to merge it because we were out on vacation. It should be available in the next canary build (tomorrow?).
Still happens even in the latest build. New plan: Ditch WinRT, and do it via Win32.

Fix patch:
diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 55f9bac881d..b77cf969f49 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -1354,6 +1354,7 @@ PNMLINK
 pntm
 POBJECT
 Podcast
+POINTERUPDATE
 POINTSLIST
 policheck
 POLYTEXTW
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 3724edd542c..d40ea2feb7f 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -47,66 +47,6 @@ constexpr std::wstring_view StateCollapsed{ L"Collapsed" };
 DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat);
 DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::MouseButtonState);
 
-// WinUI 3's UIElement.ProtectedCursor property allows someone to set the cursor on a per-element basis.
-// This would allow us to hide the cursor when the TermControl has input focus and someone starts typing.
-// Unfortunately, no equivalent exists for WinUI 2 so we fake it with the CoreWindow.
-// There are 3 downsides:
-// * SetPointerCapture() is global state and may interfere with other components.
-// * You can't start dragging the cursor (for text selection) while it's still hidden.
-// * The CoreWindow covers the union of all window rectangles, so the cursor is hidden even if it's outside
-//   the current foreground window, but still on top of another Terminal window in the background.
-static void hideCursorUntilMoved()
-{
-    static bool cursorIsHidden;
-    static const auto shouldVanish = []() {
-        BOOL shouldVanish = TRUE;
-        SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
-        if (!shouldVanish)
-        {
-            return false;
-        }
-
-        const auto window = CoreWindow::GetForCurrentThread();
-        static constexpr auto releaseCapture = [](CoreWindow window, PointerEventArgs) {
-            if (cursorIsHidden)
-            {
-                window.ReleasePointerCapture();
-            }
-        };
-        static constexpr auto restoreCursor = [](CoreWindow window, PointerEventArgs) {
-            if (cursorIsHidden)
-            {
-                cursorIsHidden = false;
-                window.PointerCursor(CoreCursor{ CoreCursorType::Arrow, 0 });
-            }
-        };
-
-        winrt::Windows::Foundation::TypedEventHandler<CoreWindow, PointerEventArgs> releaseCaptureHandler{ releaseCapture };
-        std::ignore = window.PointerMoved(releaseCaptureHandler);
-        std::ignore = window.PointerPressed(releaseCaptureHandler);
-        std::ignore = window.PointerReleased(releaseCaptureHandler);
-        std::ignore = window.PointerWheelChanged(releaseCaptureHandler);
-        std::ignore = window.PointerCaptureLost(restoreCursor);
-        return true;
-    }();
-
-    if (shouldVanish && !cursorIsHidden)
-    {
-        try
-        {
-            const auto window = CoreWindow::GetForCurrentThread();
-            window.PointerCursor(nullptr);
-            window.SetPointerCapture();
-            cursorIsHidden = true;
-        }
-        catch (...)
-        {
-            // Swallow the 0x80070057 "Failed to get pointer information." exception that randomly occurs.
-            // Curiously, it doesn't happen during the PointerCursor() but during the SetPointerCapture() call (thanks, WinUI).
-        }
-    }
-}
-
 // InputPane::GetForCurrentView() does not reliably work for XAML islands,
 // as it assumes that there's a 1:1 relationship between windows and threads.
 //
@@ -1551,8 +1491,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation
             return;
         }
 
-        hideCursorUntilMoved();
-
         const auto ch = e.Character();
         const auto keyStatus = e.KeyStatus();
         const auto scanCode = gsl::narrow_cast<WORD>(keyStatus.ScanCode);
diff --git a/src/cascadia/WindowsTerminal/IslandWindow.cpp b/src/cascadia/WindowsTerminal/IslandWindow.cpp
index 4c950cb986d..4b965be4022 100644
--- a/src/cascadia/WindowsTerminal/IslandWindow.cpp
+++ b/src/cascadia/WindowsTerminal/IslandWindow.cpp
@@ -26,6 +26,48 @@ using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
 #define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS"
 #define IDM_SYSTEM_MENU_BEGIN 0x1000
 
+// WinUI doesn't support the "Hide cursor on input" setting which is why all the modern Windows apps
+// are broken in that respect. We want to support it though, so we implement an imitation of it here.
+// We use the classic ShowCursor() API to hide it on keydown and show it on a select few messages.
+// WinUI's SetPointerCapture() cannot be used for this, because it races with internal WinUI code
+// calling that function and has proven itself to be very unreliable in practice.
+//
+// With UWP half the input stack got split off, and so most input events get rerouted through
+// the CoreInput child window running in another thread (aka InputHost aka Windows.UI.Input).
+// HideCursor() is called by WindowEmperor because WM_KEYDOWN is otherwise sent directly to that
+// CoreInput window. Same for WM_POINTERUPDATE which we use to reliably detect cursor movement.
+// WM_ACTIVATE on the other hand is only sent to each specific window and cannot be hooked by
+// inspecting the MSG struct coming from GetMessage. That's why the code must be here.
+// Best not think about this too much...
+bool IslandWindow::IsCursorHidden() noexcept
+{
+    return _cursorHidden;
+}
+
+void IslandWindow::HideCursor() noexcept
+{
+    static const auto shouldVanish = []() noexcept {
+        BOOL shouldVanish = TRUE;
+        SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
+        return shouldVanish != FALSE;
+    }();
+
+    if (!_cursorHidden && shouldVanish)
+    {
+        ShowCursor(FALSE);
+        _cursorHidden = true;
+    }
+}
+
+void IslandWindow::ShowCursorMaybe(const UINT message) noexcept
+{
+    if (_cursorHidden && (message == WM_ACTIVATE || message == WM_POINTERUPDATE))
+    {
+        _cursorHidden = false;
+        ShowCursor(TRUE);
+    }
+}
+
 IslandWindow::IslandWindow() noexcept :
     _interopWindowHandle{ nullptr },
     _rootGrid{ nullptr },
@@ -425,6 +467,11 @@ void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam
 
 [[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
 {
+    if (IsCursorHidden())
+    {
+        ShowCursorMaybe(message);
+    }
+
     switch (message)
     {
     case WM_GETMINMAXINFO:
diff --git a/src/cascadia/WindowsTerminal/IslandWindow.h b/src/cascadia/WindowsTerminal/IslandWindow.h
index 8432d4e8134..47d4e38ce98 100644
--- a/src/cascadia/WindowsTerminal/IslandWindow.h
+++ b/src/cascadia/WindowsTerminal/IslandWindow.h
@@ -14,6 +14,10 @@ class IslandWindow :
     public BaseWindow<IslandWindow>
 {
 public:
+    static bool IsCursorHidden() noexcept;
+    static void HideCursor() noexcept;
+    static void ShowCursorMaybe(const UINT message) noexcept;
+
     IslandWindow() noexcept;
     virtual ~IslandWindow() override;
 
@@ -143,7 +147,7 @@ class IslandWindow :
     bool _minimizeToNotificationArea{ false };
 
     std::unordered_map<UINT, SystemMenuItemInfo> _systemMenuItems;
-    UINT _systemMenuNextItemId;
+    UINT _systemMenuNextItemId = 0;
     void _resetSystemMenu();
 
 private:
@@ -154,4 +158,6 @@ class IslandWindow :
     // though the total height will take into account the non-client area
     // and the requirements of components hosted in the client area
     static constexpr float minimumHeight = 0;
+
+    inline static bool _cursorHidden;
 };
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
index 59f26b50b16..16e128d077b 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
@@ -417,6 +417,16 @@ void WindowEmperor::HandleCommandlineArgs(int nCmdShow)
                 _dispatchSpecialKey(msg);
                 continue;
             }
+
+            if (msg.message == WM_KEYDOWN)
+            {
+                IslandWindow::HideCursor();
+            }
+        }
+
+        if (IslandWindow::IsCursorHidden())
+        {
+            IslandWindow::ShowCursorMaybe(msg.message);
         }
 
         TranslateMessage(&msg);


Test patch: