Files
cursor-lang/CursorLang.Core/Services/RegistryStartup.cs
T
alex 6259dbd6b3
Pull request / build (pull_request) Successful in 53s
lightweight variant
2026-08-12 16:01:54 +05:00

131 lines
4.5 KiB
C#

using System.Security;
using CursorLang.Core.Models;
using Microsoft.Win32;
namespace CursorLang.Core.Services;
/// <summary>
/// Startup for a build that is not a package: a value under the Run key.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
internal sealed class RegistryStartup
{
private const string RunPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string ApprovedPath =
@"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run";
/// <summary>The name of the value — Windows shows it in the startup list.</summary>
private const string ValueName = "CursorLang";
private readonly RegistryKey _root;
private readonly string? _command;
internal RegistryStartup()
: this(Registry.CurrentUser, GetCommand())
{
}
/// <summary>A root of the test's own, so that the real startup list is left alone.</summary>
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();
}
/// <summary>
/// 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.
/// </summary>
private bool IsApprovedByUser()
{
using RegistryKey? approved = _root.OpenSubKey(ApprovedPath);
return approved?.GetValue(ValueName) is not byte[] { Length: > 0 } verdict
|| (verdict[0] & 1) == 0;
}
/// <summary>
/// What Windows is to run. <c>null</c> — the agent is not where it should be, and
/// there is nothing to write down.
/// </summary>
/// <remarks>
/// The agent by name rather than <c>Environment.ProcessPath</c>: 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 <see cref="StartupLaunch"/>. The
/// user starting the application themselves passes no such thing and gets the window.
/// </remarks>
internal static string? GetCommand() =>
AgentExecutable.AgentPath is { Length: > 0 } path
? $"\"{path}\" {StartupLaunch.Argument}"
: null;
}