diff --git a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs index 90de4e868..a0dfaf401 100644 --- a/ILSpy/Commands/CreateDiagramContextMenuEntry.cs +++ b/ILSpy/Commands/CreateDiagramContextMenuEntry.cs @@ -30,6 +30,7 @@ using ICSharpCode.ILSpyX.TreeView; using ILSpy.Docking; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; namespace ILSpy.Commands { @@ -98,35 +99,10 @@ namespace ILSpy.Commands output.WriteLine(); output.WriteLine(); var diagramHtml = Path.Combine(outputFolder, "index.html"); - output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenInShell(diagramHtml)); + output.AddButton(null, Resources.OpenExplorer, (_, _) => ShellHelper.RevealFile(diagramHtml)); output.WriteLine(); return output; } - static void OpenInShell(string path) - { - try - { - if (OperatingSystem.IsWindows()) - { - Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"") { UseShellExecute = false }); - } - else if (OperatingSystem.IsMacOS()) - { - Process.Start(new ProcessStartInfo("open", $"-R \"{path}\"") { UseShellExecute = false }); - } - else - { - // Linux: no universal "select item" command; open the parent directory instead. - var parent = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(parent)) - Process.Start(new ProcessStartInfo("xdg-open", parent) { UseShellExecute = false }); - } - } - catch - { - // Best-effort: the user can navigate to the folder manually if the shell call fails. - } - } } } diff --git a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs index 7e47b1846..8a76f1efe 100644 --- a/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs +++ b/ILSpy/Commands/ExtractPackageEntryContextMenuEntry.cs @@ -34,6 +34,7 @@ using ICSharpCode.ILSpyX.TreeView; using ILSpy.Docking; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; using static ICSharpCode.ILSpyX.LoadedPackage; @@ -133,7 +134,7 @@ namespace ILSpy.Commands output.WriteLine(); output.WriteLine(); var openTarget = isFile ? path : path; - output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenInShell(openTarget, isFile)); + output.AddButton(null, Resources.OpenExplorer, (_, _) => { if (isFile) ShellHelper.RevealFile(openTarget); else ShellHelper.OpenFolder(openTarget); }); output.WriteLine(); return output; }, token)).ConfigureAwait(true); @@ -196,31 +197,6 @@ namespace ILSpy.Commands output.WriteLine(); } - static void OpenInShell(string path, bool selectItem) - { - try - { - if (OperatingSystem.IsWindows()) - { - var args = selectItem ? $"/select,\"{path}\"" : $"\"{path}\""; - Process.Start(new ProcessStartInfo("explorer.exe", args) { UseShellExecute = false }); - } - else if (OperatingSystem.IsMacOS()) - { - var args = selectItem ? $"-R \"{path}\"" : $"\"{path}\""; - Process.Start(new ProcessStartInfo("open", args) { UseShellExecute = false }); - } - else - { - var target = selectItem ? Path.GetDirectoryName(path)! : path; - Process.Start(new ProcessStartInfo("xdg-open", target) { UseShellExecute = false }); - } - } - catch - { - // Best-effort: user can navigate manually if the shell call fails. - } - } static bool IsBundleItem(SharpTreeNode node) { diff --git a/ILSpy/Commands/OpenContainingFolderContextMenuEntry.cs b/ILSpy/Commands/OpenContainingFolderContextMenuEntry.cs index d381d9776..37efdaa9c 100644 --- a/ILSpy/Commands/OpenContainingFolderContextMenuEntry.cs +++ b/ILSpy/Commands/OpenContainingFolderContextMenuEntry.cs @@ -27,6 +27,7 @@ using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpyX.TreeView; using ILSpy.TreeNodes; +using ILSpy.Util; namespace ILSpy.Commands { @@ -46,7 +47,7 @@ namespace ILSpy.Commands public void Execute(TextViewContext context) { foreach (var path in GetPathsToReveal(context)) - RevealInFileManager(path); + ShellHelper.RevealFile(path); } /// Public for tests: returns the on-disk file paths the reveal would target, @@ -81,39 +82,5 @@ namespace ILSpy.Commands return null; } - static void RevealInFileManager(string path) - { - try - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Windows: explorer.exe /select highlights the file inside the folder. - Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{path}\"") { - UseShellExecute = false, - }); - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - // macOS: -R reveals the file in Finder. - Process.Start(new ProcessStartInfo("open", $"-R \"{path}\"") { - UseShellExecute = false, - }); - } - else - { - // Linux + others: open the parent directory; most file managers don't - // have a stable cross-distro "select-file" hook. - var dir = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(dir)) - Process.Start(new ProcessStartInfo("xdg-open", $"\"{dir}\"") { - UseShellExecute = false, - }); - } - } - catch - { - // Failure to launch the shell is non-fatal — the menu item already returned. - } - } } } diff --git a/ILSpy/Commands/PdbGenerator.cs b/ILSpy/Commands/PdbGenerator.cs index d05dd84d9..42408db7e 100644 --- a/ILSpy/Commands/PdbGenerator.cs +++ b/ILSpy/Commands/PdbGenerator.cs @@ -35,6 +35,7 @@ using ICSharpCode.ILSpyX.TreeView; using ILSpy.Docking; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; namespace ILSpy.Commands { @@ -135,27 +136,11 @@ namespace ILSpy.Commands output.Write(string.Format(Resources.GenerationCompleteInSeconds, totalWatch.Elapsed.TotalSeconds.ToString("F1"))); output.WriteLine(); output.WriteLine(); - output.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(folder)); + output.AddButton(null, Resources.OpenExplorer, (_, _) => ShellHelper.OpenFolder(folder)); output.WriteLine(); return output; }, token)).ConfigureAwait(true); } - static void OpenFolder(string path) - { - try - { - if (OperatingSystem.IsWindows()) - Process.Start(new ProcessStartInfo("explorer.exe", $"\"{path}\"") { UseShellExecute = false }); - else if (OperatingSystem.IsMacOS()) - Process.Start(new ProcessStartInfo("open", $"\"{path}\"") { UseShellExecute = false }); - else - Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); - } - catch - { - // Best-effort. - } - } } } diff --git a/ILSpy/Commands/ProjectExport.cs b/ILSpy/Commands/ProjectExport.cs index f208ff01c..74e8a2295 100644 --- a/ILSpy/Commands/ProjectExport.cs +++ b/ILSpy/Commands/ProjectExport.cs @@ -32,6 +32,7 @@ using ILSpy.Docking; using ILSpy.Languages; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; using ILSpy.Views; namespace ILSpy.Commands @@ -86,28 +87,12 @@ namespace ILSpy.Commands if (result.Success && Directory.Exists(options.OutputDirectory)) { o.AddButton(null, ICSharpCode.ILSpy.Properties.Resources.OpenExplorer, - (_, _) => OpenFolder(options.OutputDirectory)); + (_, _) => ShellHelper.OpenFolder(options.OutputDirectory)); o.WriteLine(); } return o; }); } - static void OpenFolder(string path) - { - try - { - if (OperatingSystem.IsWindows()) - Process.Start(new ProcessStartInfo("explorer.exe", $"\"{path}\"") { UseShellExecute = false }); - else if (OperatingSystem.IsMacOS()) - Process.Start(new ProcessStartInfo("open", $"\"{path}\"") { UseShellExecute = false }); - else - Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); - } - catch - { - // Best-effort. - } - } } } diff --git a/ILSpy/Commands/SaveCodeHelper.cs b/ILSpy/Commands/SaveCodeHelper.cs index 2c6d93e24..bbaa6aff8 100644 --- a/ILSpy/Commands/SaveCodeHelper.cs +++ b/ILSpy/Commands/SaveCodeHelper.cs @@ -29,6 +29,7 @@ using ILSpy.Docking; using ILSpy.Languages; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; namespace ILSpy.Commands { @@ -80,7 +81,7 @@ namespace ILSpy.Commands o.WriteLine(); if (Path.GetDirectoryName(path) is { Length: > 0 } directory) { - o.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(directory)); + o.AddButton(null, Resources.OpenExplorer, (_, _) => ShellHelper.OpenFolder(directory)); o.WriteLine(); } return o; @@ -139,21 +140,5 @@ namespace ILSpy.Commands return string.IsNullOrEmpty(clean) ? "output" : clean; } - static void OpenFolder(string path) - { - try - { - if (OperatingSystem.IsWindows()) - Process.Start(new ProcessStartInfo("explorer.exe", $"\"{path}\"") { UseShellExecute = false }); - else if (OperatingSystem.IsMacOS()) - Process.Start(new ProcessStartInfo("open", $"\"{path}\"") { UseShellExecute = false }); - else - Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); - } - catch - { - // Best-effort. - } - } } } diff --git a/ILSpy/Commands/SolutionExport.cs b/ILSpy/Commands/SolutionExport.cs index 8c0f73149..6bcaa44b5 100644 --- a/ILSpy/Commands/SolutionExport.cs +++ b/ILSpy/Commands/SolutionExport.cs @@ -31,6 +31,7 @@ using ILSpy.Docking; using ILSpy.Languages; using ILSpy.TextView; using ILSpy.TreeNodes; +using ILSpy.Util; namespace ILSpy.Commands { @@ -81,28 +82,12 @@ namespace ILSpy.Commands o.WriteLine(); if (result.Success && Path.GetDirectoryName(path) is { Length: > 0 } directory) { - o.AddButton(null, Resources.OpenExplorer, (_, _) => OpenFolder(directory)); + o.AddButton(null, Resources.OpenExplorer, (_, _) => ShellHelper.OpenFolder(directory)); o.WriteLine(); } return o; }).ConfigureAwait(true); } - static void OpenFolder(string path) - { - try - { - if (OperatingSystem.IsWindows()) - Process.Start(new ProcessStartInfo("explorer.exe", $"\"{path}\"") { UseShellExecute = false }); - else if (OperatingSystem.IsMacOS()) - Process.Start(new ProcessStartInfo("open", $"\"{path}\"") { UseShellExecute = false }); - else - Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); - } - catch - { - // Best-effort. - } - } } } diff --git a/ILSpy/Util/GraphVizGraph.cs b/ILSpy/Util/GraphVizGraph.cs index b6aebf33f..c5a89a838 100644 --- a/ILSpy/Util/GraphVizGraph.cs +++ b/ILSpy/Util/GraphVizGraph.cs @@ -66,20 +66,9 @@ namespace ILSpy.Util Process.Start("dot", $"\"{fileName}.gv\" -Tpng -o \"{fileName}.png\"")?.WaitForExit(); // Cross-platform "open file with default app": Process.Start with // UseShellExecute=true works on Windows and Mac; on Linux fall back to xdg-open. - OpenWithDefaultApplication(fileName + ".png"); + ShellHelper.OpenWithDefaultApplication(fileName + ".png"); } - static void OpenWithDefaultApplication(string path) - { - if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) - { - Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); - } - else - { - Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); - } - } static string Escape(string text) { diff --git a/ILSpy/Util/ShellHelper.cs b/ILSpy/Util/ShellHelper.cs new file mode 100644 index 000000000..f476bdcd4 --- /dev/null +++ b/ILSpy/Util/ShellHelper.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System; +using System.Diagnostics; +using System.IO; + +namespace ILSpy.Util +{ + /// + /// Cross-platform helpers for handing a path to the OS shell: open a folder, reveal a file in + /// the file manager, or open a file with its default application. All calls are best-effort -- + /// a failed launch is swallowed, since the user can navigate manually and the gesture has + /// already returned. Consolidates the explorer.exe / open / xdg-open switch that was copied + /// across the export, save, PDB, diagram and package-extraction commands. + /// + public static class ShellHelper + { + /// Opens (a directory) in the OS file manager. + public static void OpenFolder(string path) => Launch(path, selectItem: false); + + /// + /// Reveals (a file) in the OS file manager, selecting it where the + /// platform supports it (Windows /select,, macOS -R). On Linux there is no + /// stable cross-distro "select file" hook, so the parent directory is opened instead. + /// + public static void RevealFile(string path) => Launch(path, selectItem: true); + + /// Opens with its default application (image viewer, + /// browser, ...). + public static void OpenWithDefaultApplication(string path) + { + try + { + // UseShellExecute resolves the default app on Windows and macOS; Linux needs xdg-open. + if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()) + Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); + else + Process.Start(new ProcessStartInfo("xdg-open", path) { UseShellExecute = false }); + } + catch + { + // Best-effort: the user can open the file manually if the shell call fails. + } + } + + static void Launch(string path, bool selectItem) + { + try + { + if (OperatingSystem.IsWindows()) + { + var args = selectItem ? $"/select,\"{path}\"" : $"\"{path}\""; + Process.Start(new ProcessStartInfo("explorer.exe", args) { UseShellExecute = false }); + } + else if (OperatingSystem.IsMacOS()) + { + var args = selectItem ? $"-R \"{path}\"" : $"\"{path}\""; + Process.Start(new ProcessStartInfo("open", args) { UseShellExecute = false }); + } + else + { + // Linux + others: no universal "select item" command, so revealing a file opens + // its parent directory. + var target = selectItem ? (Path.GetDirectoryName(path) ?? path) : path; + Process.Start(new ProcessStartInfo("xdg-open", target) { UseShellExecute = false }); + } + } + catch + { + // Best-effort: the user can navigate manually if the shell call fails. + } + } + } +}