From 928d8e737796ec5739f9720c9dc3ea7c29a027b3 Mon Sep 17 00:00:00 2001 From: Alpaq92 Date: Thu, 9 Jul 2026 12:45:31 +0200 Subject: [PATCH] refactor: remove dead StartMenuMonitor and correct OVERVIEW's fallback description StartMenuMonitor had zero compile-time references and its HostProcessNames set is the exact false-positive pattern PR #64 removed from SystemStartMenu. The no-bridge scenario it was documented to cover is already handled by the always-installed managed hooks (StartClickHook + WinKeyHook), so OVERVIEW.md now describes that real mechanism instead. Co-Authored-By: Claude Fable 5 --- docs/OVERVIEW.md | 4 +- .../Platform/Windows/StartMenuMonitor.cs | 271 ------------------ 2 files changed, 2 insertions(+), 273 deletions(-) delete mode 100644 src/MenYou/Platform/Windows/StartMenuMonitor.cs diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index a5faa59..8bc3f9c 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -34,7 +34,7 @@ Fluid.Avalonia must live in `Application.Styles` to theme composite controls (e. ## How it works — notable mechanisms **Activation** -- *Start-button / Win-tap interception* — the taskbar Start-button **click** is caught by a managed low-level mouse hook (`StartClickHook`). The lone **Win-tap**, which on Win 11 24H2 only surfaces *inside* the shell, is caught by the optional native bridge (`MenYou.Bridge`, C++): `BridgeInjector` installs a `WH_GETMESSAGE` hook on Explorer's UI thread via the bridge DLL and forwards `WM_COPYDATA` to `CopyDataListener`. The hook is **owned by MenYou** — so Windows unloads the DLL from Explorer when MenYou exits or is killed — and the DLL ships next to `MenYou.exe` but is **shadow-copied** to `%LocalAppData%\MenYou\bridge\` before injection, so an in-place upgrade never finds Explorer holding the installed copy. If MSVC isn't installed the DLL is absent and MenYou falls back to a managed WinEvent monitor (`StartMenuMonitor`). +- *Start-button / Win-tap interception* — the taskbar Start-button **click** is caught by a managed low-level mouse hook (`StartClickHook`). The lone **Win-tap** is caught twice over: a managed low-level keyboard hook (`WinKeyHook`) swallows the real Win-down/up (buffer-and-replay, so combos still work) and fires MenYou on a lone tap, while the optional native bridge (`MenYou.Bridge`, C++) covers the tap as it surfaces *inside* the shell on Win 11 24H2: `BridgeInjector` installs a `WH_GETMESSAGE` hook on Explorer's UI thread via the bridge DLL and forwards `WM_COPYDATA` to `CopyDataListener`. The hook is **owned by MenYou** — so Windows unloads the DLL from Explorer when MenYou exits or is killed — and the DLL ships next to `MenYou.exe` but is **shadow-copied** to `%LocalAppData%\MenYou\bridge\` before injection, so an in-place upgrade never finds Explorer holding the installed copy. If MSVC isn't installed the DLL is absent, injection quietly no-ops, and the managed hooks carry the interception alone. - *Shift+Win hotkey* — registered via `RegisterHotKey`. Bare Win can't be reliably intercepted from outside the shell on Win 11 24H2, so Shift+Win is the toggle (the approach Open-Shell also recommends). - *Foreground activation* — Win 11's focus-stealing prevention leaves a freshly shown window inactive, so `Win32Foreground.Bring` does the `AttachThreadInput` → `SetForegroundWindow` dance. - *Window sizing re-measure* — the five layouts are stacked in one `SizeToContent` window gated by `IsVisible`; `ShowMenu` toggles `SizeToContent` after bindings resolve so the window fits only the active layout. @@ -76,7 +76,7 @@ dotnet build src/MenYou/MenYou.csproj src/MenYou/bin/Debug/net10.0-windows/MenYou.exe ``` -The `BuildNativeBridge` MSBuild target runs `tools/build-bridge.ps1` first; it locates VS's `msbuild` via `vswhere` and exits 0 if MSVC is missing (MenYou then runs on the managed fallback). Companion `CopyNativeBridge*` targets copy the built `MenYou.Bridge.dll` next to `MenYou.exe` for both `dotnet build` and `dotnet publish`, so it ships in the installer. A tray icon appears on launch — press **Shift+Win** to toggle the menu. +The `BuildNativeBridge` MSBuild target runs `tools/build-bridge.ps1` first; it locates VS's `msbuild` via `vswhere` and exits 0 if MSVC is missing (MenYou then runs on the managed hooks alone). Companion `CopyNativeBridge*` targets copy the built `MenYou.Bridge.dll` next to `MenYou.exe` for both `dotnet build` and `dotnet publish`, so it ships in the installer. A tray icon appears on launch — press **Shift+Win** to toggle the menu. ## Branding diff --git a/src/MenYou/Platform/Windows/StartMenuMonitor.cs b/src/MenYou/Platform/Windows/StartMenuMonitor.cs deleted file mode 100644 index 340a73a..0000000 --- a/src/MenYou/Platform/Windows/StartMenuMonitor.cs +++ /dev/null @@ -1,271 +0,0 @@ -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Runtime.Versioning; -using System.Text; - -namespace MenYou.Platform.Windows; - -/// Detects the system Start menu opening (mouse click on the taskbar Start -/// button, Ctrl+Esc, etc.), dismisses it, and fires -/// so a caller can show MenYou instead. -/// -/// On Win 10/11 the Start menu is hosted out-of-process by -/// StartMenuExperienceHost.exe. Its windows are pre-created and merely -/// hidden/shown across opens, so EVENT_OBJECT_SHOW alone is unreliable -/// (e.g. SetWindowPos-based showing won't always raise it). We install -/// two SetWinEventHook ranges: -/// - EVENT_SYSTEM_FOREGROUND — fires whenever the Start menu window -/// gains focus, which is the most reliable single signal across versions. -/// - EVENT_OBJECT_SHOW — covers the case where a host process raises -/// SHOW but the window doesn't immediately steal foreground. -/// -/// Both run on a dedicated STA thread with a message loop, as required by -/// WINEVENT_OUTOFCONTEXT. Process-name lookups are cached by PID. -[SupportedOSPlatform("windows")] -internal sealed class StartMenuMonitor : IDisposable -{ - private const uint EVENT_SYSTEM_FOREGROUND = 0x0003; - private const uint EVENT_OBJECT_SHOW = 0x8002; - private const uint WINEVENT_OUTOFCONTEXT = 0x0000; - private const uint WINEVENT_SKIPOWNPROCESS = 0x0002; - private const int OBJID_WINDOW = 0; - private const int SW_HIDE = 0; - private const uint VK_ESCAPE = 0x1B; - private const uint KEYEVENTF_KEYUP = 0x02; - private const int WM_QUIT = 0x0012; - private const int ThrottleMillis = 300; - - private static readonly HashSet HostProcessNames = new(StringComparer.OrdinalIgnoreCase) - { - "StartMenuExperienceHost", - "SearchHost", - "SearchApp", - "ShellExperienceHost", - }; - - public event Action? Intercepted; - - private readonly Thread _thread; - private readonly ManualResetEventSlim _ready = new(); - private readonly Dictionary _pidIsHost = new(); - private WinEventDelegate? _proc; - private IntPtr _hookForeground; - private IntPtr _hookShow; - private uint _threadId; - private long _lastFireTicks; - private bool _disposed; - - public StartMenuMonitor() - { - _thread = new Thread(MessageLoop) { IsBackground = true, Name = "MenYou.StartMenuMonitor" }; - _thread.SetApartmentState(ApartmentState.STA); - _thread.Start(); - _ready.Wait(); - HookTrace.Log("StartMenuMonitor: installed"); - } - - public void Dispose() - { - if (_disposed) return; - _disposed = true; - HookTrace.Log("StartMenuMonitor: disposing"); - if (_threadId != 0) PostThreadMessage(_threadId, WM_QUIT, IntPtr.Zero, IntPtr.Zero); - if (Thread.CurrentThread != _thread) _thread.Join(500); - } - - private void MessageLoop() - { - _threadId = GetCurrentThreadId(); - _proc = OnEvent; - - _hookForeground = SetWinEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND, - IntPtr.Zero, _proc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); - _hookShow = SetWinEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_SHOW, - IntPtr.Zero, _proc, 0, 0, WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS); - - _ready.Set(); - if (_hookForeground == IntPtr.Zero && _hookShow == IntPtr.Zero) - { - HookTrace.Log("StartMenuMonitor: SetWinEventHook failed for both events"); - return; - } - - while (GetMessage(out var msg, IntPtr.Zero, 0, 0)) - { - TranslateMessage(ref msg); - DispatchMessageW(ref msg); - } - - if (_hookForeground != IntPtr.Zero) UnhookWinEvent(_hookForeground); - if (_hookShow != IntPtr.Zero) UnhookWinEvent(_hookShow); - _hookForeground = _hookShow = IntPtr.Zero; - } - - private void OnEvent(IntPtr hWinEventHook, uint @event, IntPtr hwnd, - int idObject, int idChild, uint thread, uint time) - { - // Only react to top-level window events. Foreground events always have - // idObject == OBJID_WINDOW so this filter is cheap. - if (idObject != OBJID_WINDOW || idChild != 0 || hwnd == IntPtr.Zero) return; - if (!IsWindowVisible(hwnd)) return; - - if (!IsStartHostWindow(hwnd, out var processName)) - { - // In trace mode we still want to see which windows we're seeing, - // to diagnose mis-detection. Only foreground events, to keep noise - // down — SHOW is much chattier. - if (HookTrace.Enabled && @event == EVENT_SYSTEM_FOREGROUND) - HookTrace.Log($"StartMenuMonitor: ignored foreground hwnd=0x{hwnd:X} proc={processName ?? "?"} cls={GetClass(hwnd)}"); - return; - } - - var now = Environment.TickCount64; - if (now - _lastFireTicks < ThrottleMillis) return; - _lastFireTicks = now; - - HookTrace.Log($"StartMenuMonitor: intercept evt=0x{@event:X4} hwnd=0x{hwnd:X} proc={processName} cls={GetClass(hwnd)}"); - DismissStart(hwnd); - try { Intercepted?.Invoke(); } catch { /* swallow */ } - } - - private bool IsStartHostWindow(IntPtr hwnd, out string? processName) - { - processName = null; - try - { - GetWindowThreadProcessId(hwnd, out var pid); - if (pid == 0) return false; - if (_pidIsHost.TryGetValue(pid, out var cached)) - { - if (cached) processName = ProcessNameSafe(pid); - return cached; - } - - var name = ProcessNameSafe(pid); - processName = name; - var match = name is not null && HostProcessNames.Contains(name); - _pidIsHost[pid] = match; - return match; - } - catch - { - return false; - } - } - - private static string? ProcessNameSafe(uint pid) - { - try - { - using var p = Process.GetProcessById((int)pid); - return p.ProcessName; - } - catch - { - return null; - } - } - - private static string GetClass(IntPtr hwnd) - { - var sb = new StringBuilder(256); - return GetClassNameW(hwnd, sb, sb.Capacity) > 0 ? sb.ToString() : "?"; - } - - private static void DismissStart(IntPtr hwnd) - { - SendEscape(); - ShowWindow(hwnd, SW_HIDE); - } - - private static void SendEscape() - { - var inputs = new INPUT[2]; - inputs[0] = new INPUT { type = 1 }; - inputs[0].U.ki = new KEYBDINPUT { wVk = (ushort)VK_ESCAPE }; - inputs[1] = new INPUT { type = 1 }; - inputs[1].U.ki = new KEYBDINPUT { wVk = (ushort)VK_ESCAPE, dwFlags = KEYEVENTF_KEYUP }; - SendInput((uint)inputs.Length, inputs, Marshal.SizeOf()); - } - - [StructLayout(LayoutKind.Sequential)] - private struct MSG - { - public IntPtr hwnd; - public uint message; - public IntPtr wParam; - public IntPtr lParam; - public uint time; - public int pt_x; - public int pt_y; - } - - [StructLayout(LayoutKind.Sequential)] - private struct INPUT - { - public uint type; - public INPUTUNION U; - } - - [StructLayout(LayoutKind.Explicit)] - private struct INPUTUNION - { - [FieldOffset(0)] public KEYBDINPUT ki; - } - - [StructLayout(LayoutKind.Sequential)] - private struct KEYBDINPUT - { - public ushort wVk; - public ushort wScan; - public uint dwFlags; - public uint time; - public UIntPtr dwExtraInfo; - public uint padding1; - public uint padding2; - } - - private delegate void WinEventDelegate(IntPtr hWinEventHook, uint @event, IntPtr hwnd, - int idObject, int idChild, uint dwEventThread, uint dwmsEventTime); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, - WinEventDelegate pfnWinEventProc, uint idProcess, uint idThread, uint dwFlags); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool UnhookWinEvent(IntPtr hWinEventHook); - - [DllImport("user32.dll")] - private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern int GetClassNameW(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern bool GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern bool TranslateMessage(ref MSG lpMsg); - - [DllImport("user32.dll", CharSet = CharSet.Unicode)] - private static extern IntPtr DispatchMessageW(ref MSG lpMsg); - - [DllImport("kernel32.dll")] - private static extern uint GetCurrentThreadId(); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool PostThreadMessage(uint idThread, int Msg, IntPtr wParam, IntPtr lParam); - - [DllImport("user32.dll", SetLastError = true)] - private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize); -}