using System.ComponentModel; using CursorLang.Agent.Services; using CursorLang.Agent.Windows; using CursorLang.Core.Models; using CursorLang.Core.Services; using CursorLang.Core.Threading; namespace CursorLang.Agent; /// /// The background half of CursorLang: the hook, the layout polling, the popup and the /// tray icon, with a Win32 message loop underneath and no WPF anywhere. /// /// /// The services are wired by hand rather than through a container, and that is a /// decision rather than an omission: the whole point of this process is how little it /// weighs, and a container is a megabyte of assembly and a graph of reflection on the /// way to the same object. There are ten of them and they are all listed here. /// internal sealed class Agent : IDisposable { private readonly SingleInstanceGate _gate; private readonly AgentWindow _window; private readonly SettingsService _settingsService; private readonly AppSettings _settings; private readonly LocalizationService _localization; private readonly NativePopupWindow _popupWindow; private readonly LayoutPopupService _popupService; private readonly KeyboardLayoutService _layoutService; private readonly CapsLockHotkeyService _hotkeyService; private readonly LayoutNotificationCoordinator _notifications; private readonly CapsLockSwitchCoordinator _capsLock; private readonly NativeTrayIcon _tray; internal Agent(SingleInstanceGate gate) { _gate = gate; _window = new AgentWindow(); _window.AddFilter(OnWindowMessage); _settingsService = new SettingsService(); _settings = _settingsService.Load(); _localization = new LocalizationService { CurrentLanguage = _settings.Language }; _settings.PropertyChanged += OnSettingsChanged; _popupWindow = new NativePopupWindow(_settings); _popupService = new LayoutPopupService(_popupWindow, _settings); _layoutService = new KeyboardLayoutService(new KeyboardLayoutOptions()); _hotkeyService = new CapsLockHotkeyService(_settings, _window.Post); _notifications = new LayoutNotificationCoordinator(_layoutService, _popupService); _capsLock = new CapsLockSwitchCoordinator(_hotkeyService, _layoutService, _popupService, _settings); _tray = new NativeTrayIcon(_window, _localization); } /// Starts everything and pumps messages until the user asks to quit. internal int Run(bool automatic) { _gate.ActivationRequested += OnActivationRequested; _tray.OpenRequested += OnOpenRequested; _tray.ExitRequested += OnExitRequested; bool hasTray = _tray.Install(); _notifications.Start(); _capsLock.Start(); if (!automatic || !hasTray) { SettingsLauncher.Open(); } return MessageLoop.Run(); } public void Dispose() { _gate.ActivationRequested -= OnActivationRequested; _tray.OpenRequested -= OnOpenRequested; _tray.ExitRequested -= OnExitRequested; _settings.PropertyChanged -= OnSettingsChanged; _capsLock.Dispose(); _notifications.Dispose(); _hotkeyService.Dispose(); _layoutService.Dispose(); _popupService.Dispose(); _settingsService.Dispose(); _tray.Dispose(); _window.Dispose(); } // The settings window has written the file and says so. The write was a single // atomic move, so there is nothing to wait for and nothing half-written to read private bool OnWindowMessage(uint message, IntPtr wParam, IntPtr lParam) { if (message != SettingsSignal.Message) { return false; } _settingsService.Reload(); return true; } private void OnSettingsChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(AppSettings.Language)) { _localization.CurrentLanguage = _settings.Language; } } // A second launch of the agent, from the Start menu for instance. The one already // running answers the way the user expects a second launch to be answered private void OnActivationRequested(object? sender, EventArgs e) => _window.Post(() => SettingsLauncher.Open()); private void OnOpenRequested(object? sender, EventArgs e) => SettingsLauncher.Open(); private void OnExitRequested(object? sender, EventArgs e) => _window.Quit(); }