86 lines
2.8 KiB
C#
86 lines
2.8 KiB
C#
using System.Collections.Concurrent;
|
|
using CursorLang.Agent.Interop;
|
|
|
|
namespace CursorLang.Agent.Windows;
|
|
|
|
/// <summary>
|
|
/// The window the agent lives around: never shown, but it owns the tray icon and it
|
|
/// is the way back onto the message loop from a callback.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// A window with no <c>WS_VISIBLE</c> shows nowhere, yet is a window in every other
|
|
/// way. A message-only window would do as well were it not for the news of Explorer
|
|
/// restarting: that one is broadcast, and broadcasts pass such windows by.
|
|
/// </remarks>
|
|
internal sealed class AgentWindow : NativeWindow
|
|
{
|
|
/// <summary>
|
|
/// A message hook. Returning <c>true</c> means the message has been dealt with.
|
|
/// </summary>
|
|
internal delegate bool MessageFilter(uint message, IntPtr wParam, IntPtr lParam);
|
|
|
|
private const string ClassName = "CursorLang.Agent.Window";
|
|
|
|
/// <summary>Drain the queue of posted work. WM_APP is free for the application.</summary>
|
|
private const uint WM_INVOKE = WindowNative.WM_APP + 100;
|
|
|
|
private readonly List<MessageFilter> _filters = [];
|
|
private readonly ConcurrentQueue<Action> _posted = new();
|
|
|
|
internal AgentWindow()
|
|
: base(ClassName, "CursorLang agent", WindowNative.WS_POPUP, WindowNative.WS_EX_TOOLWINDOW)
|
|
{
|
|
}
|
|
|
|
internal void AddFilter(MessageFilter filter) => _filters.Add(filter);
|
|
|
|
/// <summary>
|
|
/// Runs the action on the message loop, after the current message is done with.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This is what the agent has instead of <c>Dispatcher.BeginInvoke</c>. The caller
|
|
/// that matters is the keyboard hook: Windows removes a hook whose procedure takes
|
|
/// too long, so the procedure only records what happened and the answer — showing
|
|
/// the popup, switching the layout — waits for the message after this one.
|
|
/// </remarks>
|
|
internal void Post(Action action)
|
|
{
|
|
_posted.Enqueue(action);
|
|
WindowNative.PostMessage(Handle, WM_INVOKE, IntPtr.Zero, IntPtr.Zero);
|
|
}
|
|
|
|
/// <summary>Asks the message loop to finish.</summary>
|
|
internal void Quit() => WindowNative.PostQuitMessage(0);
|
|
|
|
protected override bool OnMessage(uint message, IntPtr wParam, IntPtr lParam, out IntPtr result)
|
|
{
|
|
result = IntPtr.Zero;
|
|
|
|
if (message == WM_INVOKE)
|
|
{
|
|
while (_posted.TryDequeue(out Action? action))
|
|
{
|
|
action();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (message is WindowNative.WM_CLOSE or WindowNative.WM_ENDSESSION)
|
|
{
|
|
Quit();
|
|
return true;
|
|
}
|
|
|
|
foreach (MessageFilter filter in _filters)
|
|
{
|
|
if (filter(message, wParam, lParam))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|