using System.Collections.Concurrent;
using CursorLang.Agent.Interop;
namespace CursorLang.Agent.Windows;
///
/// 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.
///
///
/// A window with no WS_VISIBLE 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.
///
internal sealed class AgentWindow : NativeWindow
{
///
/// A message hook. Returning true means the message has been dealt with.
///
internal delegate bool MessageFilter(uint message, IntPtr wParam, IntPtr lParam);
private const string ClassName = "CursorLang.Agent.Window";
/// Drain the queue of posted work. WM_APP is free for the application.
private const uint WM_INVOKE = WindowNative.WM_APP + 100;
private readonly List _filters = [];
private readonly ConcurrentQueue _posted = new();
internal AgentWindow()
: base(ClassName, "CursorLang agent", WindowNative.WS_POPUP, WindowNative.WS_EX_TOOLWINDOW)
{
}
internal void AddFilter(MessageFilter filter) => _filters.Add(filter);
///
/// Runs the action on the message loop, after the current message is done with.
///
///
/// This is what the agent has instead of Dispatcher.BeginInvoke. 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.
///
internal void Post(Action action)
{
_posted.Enqueue(action);
WindowNative.PostMessage(Handle, WM_INVOKE, IntPtr.Zero, IntPtr.Zero);
}
/// Asks the message loop to finish.
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;
}
}