File: FolderApplication.cs
Web Access
Project: src\src\Aspire.Tray\Windows\Aspire.Tray.Windows.csproj (aspire-tray)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using Microsoft.Win32;
 
namespace Aspire.Tray;
 
/// <summary>
/// Resolves supported folder applications without executing registry command templates.
/// </summary>
[SupportedOSPlatform("windows")]
internal sealed record FolderApplication(string Title, string Executable, bool Terminal)
{
    internal static FolderApplication[] Discover(Action<string> reportError)
    {
        List<FolderApplication> applications = [];
        foreach (var (name, title, terminal) in new[]
        {
            ("Code.exe", "Visual Studio Code", false),
            ("Code - Insiders.exe", "Visual Studio Code - Insiders", false),
            ("rider64.exe", "JetBrains Rider", false),
            ("sublime_text.exe", "Sublime Text", false),
            ("WindowsTerminal.exe", "Windows Terminal", true)
        })
        {
            var path = FindAppPath(name, reportError);
            if (path is not null)
            {
                applications.Add(new(title, path, terminal));
            }
        }
        // Store applications register execution aliases rather than App Paths. Accept only
        // this exact OS-managed alias under the user's WindowsApps directory, never PATH.
        var terminalAlias = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
            "Microsoft", "WindowsApps", "wt.exe");
        if (!applications.Any(application => application.Terminal) && File.Exists(terminalAlias))
        {
            applications.Add(new("Windows Terminal", terminalAlias, true));
        }
        var powerShell = FindAppPath("pwsh.exe", reportError) ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System),
            "WindowsPowerShell", "v1.0", "powershell.exe");
        if (File.Exists(powerShell))
        {
            applications.Add(new("PowerShell", powerShell, false));
        }
        return applications.ToArray();
    }
 
    private static string? FindAppPath(string name, Action<string> reportError)
    {
        foreach (var hive in new[] { RegistryHive.CurrentUser, RegistryHive.LocalMachine })
        {
            foreach (var view in new[] { RegistryView.Registry64, RegistryView.Registry32 })
            {
                try
                {
                    using var root = RegistryKey.OpenBaseKey(hive, view);
                    using var key = root.OpenSubKey($@"Software\Microsoft\Windows\CurrentVersion\App Paths\{name}");
                    // The default App Paths value is an executable path, not a shell command:
                    // C:\Program Files\Microsoft VS Code\Code.exe (sometimes surrounded by quotes).
                    // https://learn.microsoft.com/windows/win32/shell/app-registration
                    if (key?.GetValue(null) is string value)
                    {
                        var path = value.Trim().Trim('"');
                        if (Path.IsPathFullyQualified(path) && path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) && File.Exists(path))
                        {
                            return Path.GetFullPath(path);
                        }
                        reportError($"The registered path for {name} is not an existing absolute executable.");
                    }
                }
                catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or SecurityException or ArgumentException)
                {
                    reportError($"Could not read the registered application {name}: {ex.Message}");
                }
            }
        }
        return null;
    }
 
    internal void Open(string directory)
        => Start(CreateFolderStartInfo(directory));
 
    internal ProcessStartInfo CreateFolderStartInfo(string directory)
    {
        directory = Path.GetFullPath(directory);
        if (!Directory.Exists(directory))
        {
            throw new DirectoryNotFoundException("The selected project folder no longer exists.");
        }
        var start = CreateStartInfo(Executable, directory);
        if (Terminal)
        {
            // Terminal parses ';' as a command separator even inside argv values.
            // Resolve '.' from WorkingDirectory instead of feeding paths into that grammar.
            start.ArgumentList.Add("-d");
            start.ArgumentList.Add(".");
        }
        else if (Title == "PowerShell")
        {
            // WorkingDirectory selects the folder without embedding it in PowerShell code.
            start.ArgumentList.Add("-NoLogo");
            start.ArgumentList.Add("-NoExit");
        }
        else
        {
            start.ArgumentList.Add(directory);
        }
        return start;
    }
 
    internal static void ShowInExplorer(string path)
    {
        path = TrayAppHostPath.RequireExistingFile(path);
        // Pass a shell item rather than Explorer's /select command grammar, which treats
        // commas and quotes specially. cidl=0 selects the absolute PIDL in its parent.
        // https://learn.microsoft.com/windows/win32/api/shlobj_core/nf-shlobj_core-shopenfolderandselectitems
        Marshal.ThrowExceptionForHR(NativeMethods.ParseDisplayName(path, 0, out var item, 0, out _));
        try
        {
            Marshal.ThrowExceptionForHR(NativeMethods.OpenFolderAndSelectItems(item, 0, 0, 0));
        }
        finally
        {
            Marshal.FreeCoTaskMem(item);
        }
    }
 
    private static ProcessStartInfo CreateStartInfo(string executable, string directory)
    {
        if (!Path.IsPathFullyQualified(executable) || !File.Exists(executable))
        {
            throw new FileNotFoundException("The selected application is no longer installed.", executable);
        }
        return new(executable) { UseShellExecute = false, WorkingDirectory = directory };
    }
 
    private static void Start(ProcessStartInfo start)
    {
        // The user owns the launched editor/terminal. Dispose only our process handle.
        using var process = Process.Start(start) ?? throw new InvalidOperationException("Windows could not start the selected application.");
    }
}