File: src\Aspire.Tray\Linux\LinuxTrayStartupSettings.cs
Web Access
Project: src\tests\Aspire.Tray.Tests\Aspire.Tray.Tests.csproj (Aspire.Tray.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text;
 
namespace Aspire.Tray;
 
/// <summary>
/// Opt-in XDG autostart using the stable CLI entry point, never an extracted bundle.
/// </summary>
internal sealed class LinuxTrayStartupSettings(TrayOptions options, bool nativeFrontend, string directory)
    : RegisteredTrayStartupSettings(new FileTrayStartupRegistrationStore(Path.Combine(directory, FileName)))
{
    internal const string FileName = "dev.aspire.tray.desktop";
    private const string EntryPrefix = "X-Aspire-Tray-Entry=";
 
    internal static string GetAutostartDirectory()
    {
        var config = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
        return Path.Combine(config is not null && Path.IsPathFullyQualified(config) ? config
            : Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config"), "autostart");
    }
 
    protected override string? UnavailableReason
        => TrayStartupEntry.GetUnavailableReason(options, nativeFrontend, windows: false, linux: true);
 
    protected override string CreateRegistration() => Serialize(options.StartupCliPath!);
 
    internal static string Serialize(string cli)
    {
        if (!Path.IsPathFullyQualified(cli) || cli.Any(char.IsControl))
        {
            throw new ArgumentException("Startup requires an absolute CLI path without control characters.");
        }
        // Desktop Exec is not a shell command. Quote argv, escape reserved characters,
        // then apply desktop string escaping; %% is a literal percent, not a field code.
        // For example /home/a b/$cli becomes "/home/a b/\\$cli".
        // https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html
        var argument = cli.Replace("\\", "\\\\", StringComparison.Ordinal)
            .Replace("\"", "\\\"", StringComparison.Ordinal)
            .Replace("`", "\\`", StringComparison.Ordinal)
            .Replace("$", "\\$", StringComparison.Ordinal)
            .Replace("%", "%%", StringComparison.Ordinal)
            .Replace("\\", "\\\\", StringComparison.Ordinal);
        return $"""
            [Desktop Entry]
            Type=Application
            Name=Aspire Tray
            Comment=Manage local Aspire AppHosts
            Exec="{argument}" tray start --non-interactive --nologo
            Terminal=false
            X-Aspire-Tray-Version=1
            {EntryPrefix}{Convert.ToBase64String(Encoding.UTF8.GetBytes(cli))}

            """;
    }
 
    protected override bool IsOwned(string registration)
    {
        var entries = registration.Split('\n').Where(line => line.StartsWith(EntryPrefix, StringComparison.Ordinal)).ToArray();
        if (entries.Length != 1)
        {
            return false;
        }
        try
        {
            var path = Encoding.UTF8.GetString(Convert.FromBase64String(entries[0][EntryPrefix.Length..]));
            return string.Equals(registration, Serialize(path), StringComparison.Ordinal);
        }
        catch (Exception ex) when (ex is FormatException or ArgumentException)
        {
            return false;
        }
    }
}