added statup with Windows feature

This commit is contained in:
2026-08-09 20:17:43 +05:00
parent bb19306168
commit 0c2f0bd618
4 changed files with 261 additions and 0 deletions
@@ -0,0 +1,38 @@
using System.Runtime.InteropServices;
namespace CursorLang.Interop;
/// <summary>
/// Answers whether the application runs from an MSIX package.
/// </summary>
/// <remarks>
/// The same application runs both installed from the Store and simply unpacked
/// into a folder. Some Windows features — startup through <c>StartupTask</c>, for
/// instance — are available only to a package, and reaching for them without a
/// check is not allowed: outside a package they throw.
/// </remarks>
internal static class PackageIdentityNative
{
/// <summary>APPMODEL_ERROR_NO_PACKAGE — the process runs outside a package.</summary>
private const int NoPackage = 15700;
/// <summary>
/// The application runs from an MSIX package.
/// </summary>
/// <remarks>
/// The value is computed once: it cannot change during the lifetime of
/// the process.
/// </remarks>
internal static bool IsPackaged { get; } = DetectPackage();
private static bool DetectPackage()
{
// The answer comes from the return code rather than from the name itself, so
// no buffer is needed: with zero length a package replies complaining about space
uint length = 0;
return GetCurrentPackageFullName(ref length, IntPtr.Zero) != NoPackage;
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);
}
+18
View File
@@ -0,0 +1,18 @@
using CursorLang.Models;
namespace CursorLang.Services;
/// <summary>
/// Starting the app together with Windows.
/// </summary>
public interface IStartupService
{
/// <summary>Finds out the current state of startup.</summary>
Task<StartupState> GetStateAsync();
/// <summary>
/// Asks for startup to be switched on or off and answers with the state that
/// came of it: the request to switch it on may well be turned down.
/// </summary>
Task<StartupState> SetEnabledAsync(bool enabled);
}
+120
View File
@@ -0,0 +1,120 @@
using System.IO;
using System.Security;
using CursorLang.Models;
using Microsoft.Win32;
namespace CursorLang.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 path of the running program is
/// unknown, and there is nothing to write down.
/// </summary>
private static string? GetCommand() =>
Environment.ProcessPath is { Length: > 0 } path ? $"\"{path}\"" : null;
}
+85
View File
@@ -0,0 +1,85 @@
using System.Runtime.InteropServices;
using CursorLang.Interop;
using CursorLang.Models;
using Windows.ApplicationModel;
namespace CursorLang.Services;
/// <summary>
/// Startup, arranged by whatever means the current build has.
/// </summary>
/// <remarks>
/// Startup used to be a scheduled task with the highest rights — otherwise an app
/// that wanted administrator rights would not start from the startup folder. The
/// app needs no such rights any more, and the two ways left are simpler.
///
/// A package declares the task in its manifest, and Windows lists it for the user
/// next to the rest under Settings — Apps — Startup. Turned off there, it can no
/// longer be turned back on by the app. Outside a package the same setting is kept
/// in the registry: see <see cref="RegistryStartup"/>.
/// </remarks>
public sealed class StartupService : IStartupService
{
/// <summary>Matches TaskId in the package manifest.</summary>
private const string TaskId = "CursorLangStartup";
private readonly RegistryStartup _registry = new();
public async Task<StartupState> GetStateAsync()
{
if (!PackageIdentityNative.IsPackaged)
{
return _registry.GetState();
}
try
{
StartupTask task = await StartupTask.GetAsync(TaskId);
return Translate(task.State);
}
catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
{
// No task by that name in the manifest: that happens to a package put
// together by hand. The setting simply will not show
return StartupState.Unavailable;
}
}
public async Task<StartupState> SetEnabledAsync(bool enabled)
{
if (!PackageIdentityNative.IsPackaged)
{
return _registry.SetEnabled(enabled);
}
try
{
StartupTask task = await StartupTask.GetAsync(TaskId);
if (!enabled)
{
task.Disable();
return Translate(task.State);
}
// Windows answers with a state rather than with success: once the user
// has forbidden startup, the ban stays
return Translate(await task.RequestEnableAsync());
}
catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
{
return StartupState.Unavailable;
}
}
/// <summary>The state of a Windows task in the app's own terms.</summary>
internal static StartupState Translate(StartupTaskState state) => state switch
{
StartupTaskState.Enabled => StartupState.Enabled,
StartupTaskState.EnabledByPolicy => StartupState.EnabledByPolicy,
StartupTaskState.Disabled => StartupState.Disabled,
StartupTaskState.DisabledByUser => StartupState.DisabledByUser,
StartupTaskState.DisabledByPolicy => StartupState.DisabledByPolicy,
_ => StartupState.Unavailable,
};
}