using System.Security; using CursorLang.Core.Models; using Microsoft.Win32; namespace CursorLang.Core.Services; /// /// Startup for a build that is not a package: a value under the Run key. /// /// /// A package declares its startup task in the manifest and asks Windows to switch /// it on. A build unpacked into a folder has no manifest, so it registers itself /// the way desktop programs always have — under the Run key of the current user. /// Administrator rights are not needed for that: the key belongs to the user. /// /// Windows keeps the user's own verdict apart from the entry itself. Turning the /// app off in Settings — Apps — Startup leaves the Run value where it is and marks /// it disabled under StartupApproved. The mark is obeyed here the same way a /// package obeys DisabledByUser: the app does not argue with the user. /// internal sealed class RegistryStartup { private const string RunPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; private const string ApprovedPath = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run"; /// The name of the value — Windows shows it in the startup list. private const string ValueName = "CursorLang"; private readonly RegistryKey _root; private readonly string? _command; internal RegistryStartup() : this(Registry.CurrentUser, GetCommand()) { } /// A root of the test's own, so that the real startup list is left alone. internal RegistryStartup(RegistryKey root, string? command) { _root = root; _command = command; } internal StartupState GetState() { if (_command is null) { return StartupState.Unavailable; } try { using RegistryKey? run = _root.OpenSubKey(RunPath); if (run?.GetValue(ValueName) is null) { return StartupState.Disabled; } return IsApprovedByUser() ? StartupState.Enabled : StartupState.DisabledByUser; } catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException) { return StartupState.Unavailable; } } internal StartupState SetEnabled(bool enabled) { if (_command is null) { return StartupState.Unavailable; } try { using RegistryKey run = _root.CreateSubKey(RunPath); if (enabled) { // The path is written afresh every time: the app may have been moved run.SetValue(ValueName, _command, RegistryValueKind.String); } else { run.DeleteValue(ValueName, throwOnMissingValue: false); } } catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException) { return StartupState.Unavailable; } // The answer is read back rather than assumed: an entry the user has // banned stays banned no matter what was just written next to it return GetState(); } /// /// Whether the user has left the entry alone. The verdict is a blob whose /// lowest bit of the first byte stands for the ban; no value means untouched. /// private bool IsApprovedByUser() { using RegistryKey? approved = _root.OpenSubKey(ApprovedPath); return approved?.GetValue(ValueName) is not byte[] { Length: > 0 } verdict || (verdict[0] & 1) == 0; } /// /// What Windows is to run. null — the agent is not where it should be, and /// there is nothing to write down. /// /// /// The agent by name rather than Environment.ProcessPath: this setting is /// switched from the settings window, and its own path would put the wrong process /// into the startup list — one that shows a window and exits. /// /// The argument is how the agent recognises a launch of this kind and goes straight /// to the tray without the settings window: see . The /// user starting the application themselves passes no such thing and gets the window. /// internal static string? GetCommand() => AgentExecutable.AgentPath is { Length: > 0 } path ? $"\"{path}\" {StartupLaunch.Argument}" : null; }