From 76b4aa8bf812e2d58518198d9b6851b77542c87c Mon Sep 17 00:00:00 2001
From: Andrew Clinick <80841394+aclinick@users.noreply.github.com>
Date: Fri, 17 Jul 2026 10:42:22 -0700
Subject: [PATCH] Add 'Open install folder in Terminal' to installed apps
Right-clicking an app in the Apps pane now offers 'Open install folder
in Terminal', which opens that package's install location in a terminal
so users can inspect what's inside.
TerminalLauncher (Core, testable) returns an ordered list of launch
candidates: Windows Terminal (wt.exe -d), then PowerShell, then cmd.exe.
The UI tries each until one starts, so machines without Windows Terminal
still fall back to a shell that always ships with Windows. Launch uses
ProcessStartInfo with UseShellExecute=true (allowed via runFullTrust).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../TerminalLauncherTests.cs | 56 +++++++++++++++++++
.../Models/TerminalLaunchOption.cs | 14 +++++
MSIXplainer.Core/Services/TerminalLauncher.cs | 35 ++++++++++++
MSIXplainer/MainPage.xaml | 10 ++++
MSIXplainer/MainPage.xaml.cs | 32 +++++++++++
5 files changed, 147 insertions(+)
create mode 100644 MSIXplainer.Core.Tests/TerminalLauncherTests.cs
create mode 100644 MSIXplainer.Core/Models/TerminalLaunchOption.cs
create mode 100644 MSIXplainer.Core/Services/TerminalLauncher.cs
diff --git a/MSIXplainer.Core.Tests/TerminalLauncherTests.cs b/MSIXplainer.Core.Tests/TerminalLauncherTests.cs
new file mode 100644
index 0000000..ad5fb42
--- /dev/null
+++ b/MSIXplainer.Core.Tests/TerminalLauncherTests.cs
@@ -0,0 +1,56 @@
+using MSIXplainer.Services;
+using Xunit;
+
+namespace MSIXplainer.Core.Tests;
+
+public class TerminalLauncherTests
+{
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void GetLaunchOptions_BlankPath_ReturnsEmpty(string? path)
+ {
+ Assert.Empty(TerminalLauncher.GetLaunchOptions(path));
+ }
+
+ [Fact]
+ public void GetLaunchOptions_OrdersWindowsTerminalFirstThenPowerShellThenCmd()
+ {
+ var options = TerminalLauncher.GetLaunchOptions(@"C:\apps\myapp");
+
+ Assert.Equal(3, options.Count);
+ Assert.Equal("wt.exe", options[0].FileName);
+ Assert.Equal("powershell.exe", options[1].FileName);
+ Assert.Equal("cmd.exe", options[2].FileName);
+ }
+
+ [Fact]
+ public void GetLaunchOptions_WindowsTerminal_EncodesFolderInQuotedDArgument()
+ {
+ var wt = TerminalLauncher.GetLaunchOptions(@"C:\apps\my app").First();
+
+ Assert.Equal("-d \"C:\\apps\\my app\"", wt.Arguments);
+ Assert.Null(wt.WorkingDirectory);
+ }
+
+ [Fact]
+ public void GetLaunchOptions_PowerShellAndCmd_UseWorkingDirectory()
+ {
+ var options = TerminalLauncher.GetLaunchOptions(@"C:\apps\myapp");
+
+ Assert.Equal(@"C:\apps\myapp", options[1].WorkingDirectory);
+ Assert.Equal("-NoExit", options[1].Arguments);
+
+ Assert.Equal(@"C:\apps\myapp", options[2].WorkingDirectory);
+ Assert.Null(options[2].Arguments);
+ }
+
+ [Fact]
+ public void GetLaunchOptions_TrimsTrailingSeparator()
+ {
+ var wt = TerminalLauncher.GetLaunchOptions(@"C:\apps\myapp\").First();
+
+ Assert.Equal("-d \"C:\\apps\\myapp\"", wt.Arguments);
+ }
+}
diff --git a/MSIXplainer.Core/Models/TerminalLaunchOption.cs b/MSIXplainer.Core/Models/TerminalLaunchOption.cs
new file mode 100644
index 0000000..5577ac1
--- /dev/null
+++ b/MSIXplainer.Core/Models/TerminalLaunchOption.cs
@@ -0,0 +1,14 @@
+namespace MSIXplainer.Models;
+
+///
+/// A single terminal launch candidate produced by
+/// . The UI layer maps this onto a
+/// ProcessStartInfo with UseShellExecute = true.
+///
+/// Executable to launch (e.g. wt.exe).
+/// Command-line arguments, or null for none.
+///
+/// Working directory to start in, or null when the folder is already
+/// encoded in (as it is for Windows Terminal).
+///
+public sealed record TerminalLaunchOption(string FileName, string? Arguments, string? WorkingDirectory);
diff --git a/MSIXplainer.Core/Services/TerminalLauncher.cs b/MSIXplainer.Core/Services/TerminalLauncher.cs
new file mode 100644
index 0000000..e9c4072
--- /dev/null
+++ b/MSIXplainer.Core/Services/TerminalLauncher.cs
@@ -0,0 +1,35 @@
+using MSIXplainer.Models;
+
+namespace MSIXplainer.Services;
+
+///
+/// Builds an ordered list of terminal launch candidates for a folder. The UI
+/// layer tries each candidate in turn until one starts, so a machine without
+/// Windows Terminal still falls back to PowerShell and finally cmd.exe (both
+/// of which ship with every supported Windows release).
+///
+public static class TerminalLauncher
+{
+ ///
+ /// Returns the terminals to try, most-preferred first, for opening
+ /// . Returns an empty list when the path is
+ /// null/blank so callers can skip launching entirely.
+ ///
+ public static IReadOnlyList GetLaunchOptions(string? folderPath)
+ {
+ if (string.IsNullOrWhiteSpace(folderPath))
+ return [];
+
+ var path = folderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ return
+ [
+ // Windows Terminal opens directly in the folder via -d.
+ new TerminalLaunchOption("wt.exe", $"-d \"{path}\"", WorkingDirectory: null),
+ // PowerShell always ships with Windows; -NoExit keeps the window open.
+ new TerminalLaunchOption("powershell.exe", "-NoExit", WorkingDirectory: path),
+ // cmd.exe is the last-resort fallback.
+ new TerminalLaunchOption("cmd.exe", Arguments: null, WorkingDirectory: path),
+ ];
+ }
+}
diff --git a/MSIXplainer/MainPage.xaml b/MSIXplainer/MainPage.xaml
index 3a0b7a7..84ba79a 100644
--- a/MSIXplainer/MainPage.xaml
+++ b/MSIXplainer/MainPage.xaml
@@ -97,6 +97,16 @@
+
+
+
+
+
+
+
+
+
diff --git a/MSIXplainer/MainPage.xaml.cs b/MSIXplainer/MainPage.xaml.cs
index b61f4e4..75acf8f 100644
--- a/MSIXplainer/MainPage.xaml.cs
+++ b/MSIXplainer/MainPage.xaml.cs
@@ -178,6 +178,38 @@ private void OnInstalledAppClick(object sender, ItemClickEventArgs e)
}
}
+ // Opens the selected app's install folder in a terminal. Tries Windows
+ // Terminal first, then falls back to PowerShell / cmd (see TerminalLauncher).
+ private void OnOpenInstallFolderInTerminal(object sender, RoutedEventArgs e)
+ {
+ if (sender is not FrameworkElement { DataContext: InstalledPackage pkg })
+ return;
+
+ foreach (var option in Services.TerminalLauncher.GetLaunchOptions(pkg.InstallLocation))
+ {
+ try
+ {
+ var psi = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = option.FileName,
+ UseShellExecute = true,
+ };
+ if (!string.IsNullOrEmpty(option.Arguments))
+ psi.Arguments = option.Arguments;
+ if (!string.IsNullOrEmpty(option.WorkingDirectory))
+ psi.WorkingDirectory = option.WorkingDirectory;
+
+ System.Diagnostics.Process.Start(psi);
+ return;
+ }
+ catch (System.Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine(
+ $"[MSIXplainer] Terminal launch '{option.FileName}' failed: {ex.Message}");
+ }
+ }
+ }
+
private void EnterCompareMode()
{
ViewModel.IsCompareMode = true;