From 438bb8a2fcdb6fd96258da5b9b0c5bca8905e47e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:43:35 +0000 Subject: [PATCH 01/18] Initial plan From d61ec1828f89e78c2f55cd2475650179cc789688 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:47:57 +0000 Subject: [PATCH 02/18] Add relative path resolution support for Python and Node.js executables Co-authored-by: Jack251970 <53996452+Jack251970@users.noreply.github.com> --- .../Environments/AbstractPluginEnvironment.cs | 21 +++- Flow.Launcher.Infrastructure/Constant.cs | 20 ++++ Flow.Launcher.Test/PathResolutionTest.cs | 104 ++++++++++++++++++ 3 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 Flow.Launcher.Test/PathResolutionTest.cs diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 1a324a9930a..c3703081290 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Windows; using System.Windows.Forms; +using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; @@ -40,6 +41,12 @@ internal AbstractPluginEnvironment(List pluginMetadataList, Plug PluginSettings = pluginSettings; } + /// + /// Resolves the configured executable path to an absolute path. + /// Supports both absolute paths and relative paths (relative to ProgramDirectory). + /// + private string ResolvedPluginsSettingsFilePath => Constant.ResolveAbsolutePath(PluginsSettingsFilePath); + internal IEnumerable Setup() { // If no plugin is using the language, return empty list @@ -48,13 +55,14 @@ internal IEnumerable Setup() return new List(); } - if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) + var resolvedPath = ResolvedPluginsSettingsFilePath; + if (!string.IsNullOrEmpty(resolvedPath) && FilesFolders.FileExists(resolvedPath)) { // Ensure latest only if user is using Flow's environment setup. - if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) - EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath); + if (resolvedPath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) + EnsureLatestInstalled(ExecutablePath, resolvedPath, EnvPath); - return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + return SetPathForPluginPairs(resolvedPath, Language); } var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine); @@ -103,9 +111,10 @@ internal IEnumerable Setup() InstallEnvironment(); } - if (FilesFolders.FileExists(PluginsSettingsFilePath)) + resolvedPath = ResolvedPluginsSettingsFilePath; + if (FilesFolders.FileExists(resolvedPath)) { - return SetPathForPluginPairs(PluginsSettingsFilePath, Language); + return SetPathForPluginPairs(resolvedPath, Language); } else { diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 13da9f79f3b..57175930fe2 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -56,5 +56,25 @@ public static class Constant public const string Docs = "https://flowlauncher.com/docs"; public const string SystemLanguageCode = "system"; + + /// + /// Resolves a path that may be relative to an absolute path. + /// If the path is already absolute, returns it as-is. + /// If the path is relative (starts with . or doesn't contain a drive), resolves it relative to ProgramDirectory. + /// + /// The path to resolve + /// An absolute path + public static string ResolveAbsolutePath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // If already absolute, return as-is + if (Path.IsPathRooted(path)) + return path; + + // Resolve relative to ProgramDirectory + return Path.GetFullPath(Path.Combine(ProgramDirectory, path)); + } } } diff --git a/Flow.Launcher.Test/PathResolutionTest.cs b/Flow.Launcher.Test/PathResolutionTest.cs new file mode 100644 index 00000000000..4e03a2d6f40 --- /dev/null +++ b/Flow.Launcher.Test/PathResolutionTest.cs @@ -0,0 +1,104 @@ +using System; +using System.IO; +using Xunit; +using Flow.Launcher.Infrastructure; + +namespace Flow.Launcher.Test +{ + public class PathResolutionTest + { + [Fact] + public void ResolveAbsolutePath_WithAbsolutePath_ReturnsOriginalPath() + { + // Arrange + var absolutePath = @"C:\Program Files\Python\python.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(absolutePath); + + // Assert + Assert.Equal(absolutePath, result); + } + + [Fact] + public void ResolveAbsolutePath_WithRelativePath_ResolvesToProgramDirectory() + { + // Arrange + var relativePath = @".\runtimes\python\pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + Assert.Contains(Constant.ProgramDirectory, result); + Assert.EndsWith(@"runtimes\python\pythonw.exe", result); + } + + [Fact] + public void ResolveAbsolutePath_WithDotDotPath_ResolvesCorrectly() + { + // Arrange + var relativePath = @"..\runtimes\node\node.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + } + + [Fact] + public void ResolveAbsolutePath_WithNullPath_ReturnsNull() + { + // Arrange + string nullPath = null; + + // Act + var result = Constant.ResolveAbsolutePath(nullPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ResolveAbsolutePath_WithEmptyPath_ReturnsEmpty() + { + // Arrange + var emptyPath = string.Empty; + + // Act + var result = Constant.ResolveAbsolutePath(emptyPath); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void ResolveAbsolutePath_WithForwardSlashes_ResolvesCorrectly() + { + // Arrange + var relativePath = @"./runtimes/python/pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(relativePath); + + // Assert + Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); + Assert.Contains(Constant.ProgramDirectory, result); + } + + [Fact] + public void ResolveAbsolutePath_WithUNCPath_ReturnsOriginalPath() + { + // Arrange + var uncPath = @"\\server\share\python\pythonw.exe"; + + // Act + var result = Constant.ResolveAbsolutePath(uncPath); + + // Assert + Assert.Equal(uncPath, result); + } + } +} From 864eedd255410baaa8bb3f405d010f9804ee766d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 07:49:13 +0000 Subject: [PATCH 03/18] Add UI descriptions for relative path support Co-authored-by: Jack251970 <53996452+Jack251970@users.noreply.github.com> --- .../SettingPages/Views/SettingsPaneGeneral.xaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index b4c94cb354b..8900a16ceda 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -476,7 +476,10 @@ Content="{Binding Settings.CustomBrowser.DisplayName}" /> - + - + Date: Sat, 24 Jan 2026 07:51:22 +0000 Subject: [PATCH 04/18] Address code review feedback: convert absolute to relative paths for portability Co-authored-by: Jack251970 <53996452+Jack251970@users.noreply.github.com> --- .../Environments/AbstractPluginEnvironment.cs | 8 +- Flow.Launcher.Infrastructure/Constant.cs | 41 +++++++++ Flow.Launcher.Test/PathResolutionTest.cs | 84 +++++++++++++++++++ .../SettingsPaneGeneralViewModel.cs | 6 +- 4 files changed, 134 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index c3703081290..3a9732aa300 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -42,7 +42,7 @@ internal AbstractPluginEnvironment(List pluginMetadataList, Plug } /// - /// Resolves the configured executable path to an absolute path. + /// Resolves the configured plugin settings file path to an absolute path. /// Supports both absolute paths and relative paths (relative to ProgramDirectory). /// private string ResolvedPluginsSettingsFilePath => Constant.ResolveAbsolutePath(PluginsSettingsFilePath); @@ -74,7 +74,8 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { - PluginsSettingsFilePath = selectedFile; + // Convert to relative path if within ProgramDirectory for portability + PluginsSettingsFilePath = Constant.ConvertToRelativePathIfPossible(selectedFile); } // Nothing selected because user pressed cancel from the file dialog window else @@ -98,7 +99,8 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { - PluginsSettingsFilePath = selectedFile; + // Convert to relative path if within ProgramDirectory for portability + PluginsSettingsFilePath = Constant.ConvertToRelativePathIfPossible(selectedFile); } else { diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index 57175930fe2..fd153864636 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -76,5 +76,46 @@ public static string ResolveAbsolutePath(string path) // Resolve relative to ProgramDirectory return Path.GetFullPath(Path.Combine(ProgramDirectory, path)); } + + /// + /// Converts an absolute path to a relative path if it's within ProgramDirectory. + /// This enables portability by storing paths relative to the program directory when possible. + /// + /// The absolute path to convert + /// A relative path if the path is within ProgramDirectory, otherwise the original absolute path + public static string ConvertToRelativePathIfPossible(string absolutePath) + { + if (string.IsNullOrEmpty(absolutePath)) + return absolutePath; + + if (!Path.IsPathRooted(absolutePath)) + return absolutePath; + + try + { + // Get the full absolute paths for comparison + var fullAbsolutePath = Path.GetFullPath(absolutePath); + var fullProgramDir = Path.GetFullPath(ProgramDirectory); + + // Check if the absolute path is within ProgramDirectory + if (fullAbsolutePath.StartsWith(fullProgramDir, StringComparison.OrdinalIgnoreCase)) + { + // Convert to relative path + var relativePath = Path.GetRelativePath(fullProgramDir, fullAbsolutePath); + + // Prefix with .\ for clarity + if (!relativePath.StartsWith(".")) + relativePath = ".\\" + relativePath; + + return relativePath; + } + } + catch + { + // If conversion fails, return the original path + } + + return absolutePath; + } } } diff --git a/Flow.Launcher.Test/PathResolutionTest.cs b/Flow.Launcher.Test/PathResolutionTest.cs index 4e03a2d6f40..3888b41a4e4 100644 --- a/Flow.Launcher.Test/PathResolutionTest.cs +++ b/Flow.Launcher.Test/PathResolutionTest.cs @@ -100,5 +100,89 @@ public void ResolveAbsolutePath_WithUNCPath_ReturnsOriginalPath() // Assert Assert.Equal(uncPath, result); } + + [Fact] + public void ConvertToRelativePathIfPossible_WithPathInProgramDirectory_ReturnsRelativePath() + { + // Arrange + var absolutePath = Path.Combine(Constant.ProgramDirectory, "runtimes", "python", "pythonw.exe"); + + // Act + var result = Constant.ConvertToRelativePathIfPossible(absolutePath); + + // Assert + Assert.True(result.StartsWith(".\\"), "Result should start with .\\"); + Assert.Contains("runtimes", result); + Assert.Contains("python", result); + } + + [Fact] + public void ConvertToRelativePathIfPossible_WithPathOutsideProgramDirectory_ReturnsAbsolutePath() + { + // Arrange + var absolutePath = @"C:\Python\python.exe"; + + // Act + var result = Constant.ConvertToRelativePathIfPossible(absolutePath); + + // Assert + Assert.Equal(absolutePath, result); + } + + [Fact] + public void ConvertToRelativePathIfPossible_WithRelativePath_ReturnsOriginalPath() + { + // Arrange + var relativePath = @".\runtimes\python\pythonw.exe"; + + // Act + var result = Constant.ConvertToRelativePathIfPossible(relativePath); + + // Assert + Assert.Equal(relativePath, result); + } + + [Fact] + public void ConvertToRelativePathIfPossible_WithNullPath_ReturnsNull() + { + // Arrange + string nullPath = null; + + // Act + var result = Constant.ConvertToRelativePathIfPossible(nullPath); + + // Assert + Assert.Null(result); + } + + [Fact] + public void ConvertToRelativePathIfPossible_WithEmptyPath_ReturnsEmpty() + { + // Arrange + var emptyPath = string.Empty; + + // Act + var result = Constant.ConvertToRelativePathIfPossible(emptyPath); + + // Assert + Assert.Equal(string.Empty, result); + } + + [Fact] + public void RoundTripTest_RelativePathResolutionAndConversion() + { + // Arrange + var originalRelative = @".\runtimes\python\pythonw.exe"; + + // Act - Resolve to absolute + var absolute = Constant.ResolveAbsolutePath(originalRelative); + // Convert back to relative + var backToRelative = Constant.ConvertToRelativePathIfPossible(absolute); + + // Assert + Assert.True(Path.IsPathRooted(absolute), "Resolved path should be absolute"); + Assert.True(backToRelative.StartsWith(".\\"), "Converted path should be relative"); + Assert.Contains("runtimes\\python\\pythonw.exe", backToRelative); + } } } diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index aa78849bad9..eb00ae3ea4d 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -377,7 +377,8 @@ private void SelectPython() ); if (!string.IsNullOrEmpty(selectedFile)) - Settings.PluginSettings.PythonExecutablePath = selectedFile; + // Convert to relative path if within ProgramDirectory for portability + Settings.PluginSettings.PythonExecutablePath = Constant.ConvertToRelativePathIfPossible(selectedFile); } [RelayCommand] @@ -389,7 +390,8 @@ private void SelectNode() ); if (!string.IsNullOrEmpty(selectedFile)) - Settings.PluginSettings.NodeExecutablePath = selectedFile; + // Convert to relative path if within ProgramDirectory for portability + Settings.PluginSettings.NodeExecutablePath = Constant.ConvertToRelativePathIfPossible(selectedFile); } [RelayCommand] From ab5a0b493ddda6facc6b8b21dfc8d95490615e3c Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 24 Jan 2026 15:55:49 +0800 Subject: [PATCH 05/18] Revert "Add UI descriptions for relative path support" This reverts commit 864eedd255410baaa8bb3f405d010f9804ee766d. --- .../SettingPages/Views/SettingsPaneGeneral.xaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 8900a16ceda..b4c94cb354b 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -476,10 +476,7 @@ Content="{Binding Settings.CustomBrowser.DisplayName}" /> - + - + Date: Sat, 24 Jan 2026 15:57:52 +0800 Subject: [PATCH 06/18] Save Python/Node paths as absolute, not relative Removed logic that converted selected Python and Node executable paths to relative if within the program directory. Now, the selected file paths are stored as absolute paths without conversion. This simplifies path handling and improves clarity. --- .../SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index eb00ae3ea4d..aa78849bad9 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -377,8 +377,7 @@ private void SelectPython() ); if (!string.IsNullOrEmpty(selectedFile)) - // Convert to relative path if within ProgramDirectory for portability - Settings.PluginSettings.PythonExecutablePath = Constant.ConvertToRelativePathIfPossible(selectedFile); + Settings.PluginSettings.PythonExecutablePath = selectedFile; } [RelayCommand] @@ -390,8 +389,7 @@ private void SelectNode() ); if (!string.IsNullOrEmpty(selectedFile)) - // Convert to relative path if within ProgramDirectory for portability - Settings.PluginSettings.NodeExecutablePath = Constant.ConvertToRelativePathIfPossible(selectedFile); + Settings.PluginSettings.NodeExecutablePath = selectedFile; } [RelayCommand] From 9070ff4c7bcce6f1daf2b413b8351324727b0ffd Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 24 Jan 2026 15:58:37 +0800 Subject: [PATCH 07/18] Remove PathResolutionTest.cs and related path tests Removed the PathResolutionTest.cs file, which contained unit tests for the Constant class's path resolution methods. These tests covered absolute, relative, UNC, null, and empty path scenarios, as well as round-trip conversions. --- Flow.Launcher.Test/PathResolutionTest.cs | 188 ----------------------- 1 file changed, 188 deletions(-) delete mode 100644 Flow.Launcher.Test/PathResolutionTest.cs diff --git a/Flow.Launcher.Test/PathResolutionTest.cs b/Flow.Launcher.Test/PathResolutionTest.cs deleted file mode 100644 index 3888b41a4e4..00000000000 --- a/Flow.Launcher.Test/PathResolutionTest.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.IO; -using Xunit; -using Flow.Launcher.Infrastructure; - -namespace Flow.Launcher.Test -{ - public class PathResolutionTest - { - [Fact] - public void ResolveAbsolutePath_WithAbsolutePath_ReturnsOriginalPath() - { - // Arrange - var absolutePath = @"C:\Program Files\Python\python.exe"; - - // Act - var result = Constant.ResolveAbsolutePath(absolutePath); - - // Assert - Assert.Equal(absolutePath, result); - } - - [Fact] - public void ResolveAbsolutePath_WithRelativePath_ResolvesToProgramDirectory() - { - // Arrange - var relativePath = @".\runtimes\python\pythonw.exe"; - - // Act - var result = Constant.ResolveAbsolutePath(relativePath); - - // Assert - Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); - Assert.Contains(Constant.ProgramDirectory, result); - Assert.EndsWith(@"runtimes\python\pythonw.exe", result); - } - - [Fact] - public void ResolveAbsolutePath_WithDotDotPath_ResolvesCorrectly() - { - // Arrange - var relativePath = @"..\runtimes\node\node.exe"; - - // Act - var result = Constant.ResolveAbsolutePath(relativePath); - - // Assert - Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); - } - - [Fact] - public void ResolveAbsolutePath_WithNullPath_ReturnsNull() - { - // Arrange - string nullPath = null; - - // Act - var result = Constant.ResolveAbsolutePath(nullPath); - - // Assert - Assert.Null(result); - } - - [Fact] - public void ResolveAbsolutePath_WithEmptyPath_ReturnsEmpty() - { - // Arrange - var emptyPath = string.Empty; - - // Act - var result = Constant.ResolveAbsolutePath(emptyPath); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void ResolveAbsolutePath_WithForwardSlashes_ResolvesCorrectly() - { - // Arrange - var relativePath = @"./runtimes/python/pythonw.exe"; - - // Act - var result = Constant.ResolveAbsolutePath(relativePath); - - // Assert - Assert.True(Path.IsPathRooted(result), "Result should be an absolute path"); - Assert.Contains(Constant.ProgramDirectory, result); - } - - [Fact] - public void ResolveAbsolutePath_WithUNCPath_ReturnsOriginalPath() - { - // Arrange - var uncPath = @"\\server\share\python\pythonw.exe"; - - // Act - var result = Constant.ResolveAbsolutePath(uncPath); - - // Assert - Assert.Equal(uncPath, result); - } - - [Fact] - public void ConvertToRelativePathIfPossible_WithPathInProgramDirectory_ReturnsRelativePath() - { - // Arrange - var absolutePath = Path.Combine(Constant.ProgramDirectory, "runtimes", "python", "pythonw.exe"); - - // Act - var result = Constant.ConvertToRelativePathIfPossible(absolutePath); - - // Assert - Assert.True(result.StartsWith(".\\"), "Result should start with .\\"); - Assert.Contains("runtimes", result); - Assert.Contains("python", result); - } - - [Fact] - public void ConvertToRelativePathIfPossible_WithPathOutsideProgramDirectory_ReturnsAbsolutePath() - { - // Arrange - var absolutePath = @"C:\Python\python.exe"; - - // Act - var result = Constant.ConvertToRelativePathIfPossible(absolutePath); - - // Assert - Assert.Equal(absolutePath, result); - } - - [Fact] - public void ConvertToRelativePathIfPossible_WithRelativePath_ReturnsOriginalPath() - { - // Arrange - var relativePath = @".\runtimes\python\pythonw.exe"; - - // Act - var result = Constant.ConvertToRelativePathIfPossible(relativePath); - - // Assert - Assert.Equal(relativePath, result); - } - - [Fact] - public void ConvertToRelativePathIfPossible_WithNullPath_ReturnsNull() - { - // Arrange - string nullPath = null; - - // Act - var result = Constant.ConvertToRelativePathIfPossible(nullPath); - - // Assert - Assert.Null(result); - } - - [Fact] - public void ConvertToRelativePathIfPossible_WithEmptyPath_ReturnsEmpty() - { - // Arrange - var emptyPath = string.Empty; - - // Act - var result = Constant.ConvertToRelativePathIfPossible(emptyPath); - - // Assert - Assert.Equal(string.Empty, result); - } - - [Fact] - public void RoundTripTest_RelativePathResolutionAndConversion() - { - // Arrange - var originalRelative = @".\runtimes\python\pythonw.exe"; - - // Act - Resolve to absolute - var absolute = Constant.ResolveAbsolutePath(originalRelative); - // Convert back to relative - var backToRelative = Constant.ConvertToRelativePathIfPossible(absolute); - - // Assert - Assert.True(Path.IsPathRooted(absolute), "Resolved path should be absolute"); - Assert.True(backToRelative.StartsWith(".\\"), "Converted path should be relative"); - Assert.Contains("runtimes\\python\\pythonw.exe", backToRelative); - } - } -} From d16e43de8a0717429085015008542e53f7e4307d Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 24 Jan 2026 16:03:26 +0800 Subject: [PATCH 08/18] Refactor path utilities to DataLocation from Constant Move ResolveAbsolutePath and ConvertToRelativePathIfPossible from Constant to DataLocation for better organization. Update all references accordingly; implementations remain unchanged. This improves code clarity around file path management. --- .../Environments/AbstractPluginEnvironment.cs | 6 +- Flow.Launcher.Infrastructure/Constant.cs | 61 ------------------- .../UserSettings/DataLocation.cs | 61 +++++++++++++++++++ 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 3a9732aa300..e08a5815e85 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -45,7 +45,7 @@ internal AbstractPluginEnvironment(List pluginMetadataList, Plug /// Resolves the configured plugin settings file path to an absolute path. /// Supports both absolute paths and relative paths (relative to ProgramDirectory). /// - private string ResolvedPluginsSettingsFilePath => Constant.ResolveAbsolutePath(PluginsSettingsFilePath); + private string ResolvedPluginsSettingsFilePath => DataLocation.ResolveAbsolutePath(PluginsSettingsFilePath); internal IEnumerable Setup() { @@ -75,7 +75,7 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { // Convert to relative path if within ProgramDirectory for portability - PluginsSettingsFilePath = Constant.ConvertToRelativePathIfPossible(selectedFile); + PluginsSettingsFilePath = DataLocation.ConvertToRelativePathIfPossible(selectedFile); } // Nothing selected because user pressed cancel from the file dialog window else @@ -100,7 +100,7 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { // Convert to relative path if within ProgramDirectory for portability - PluginsSettingsFilePath = Constant.ConvertToRelativePathIfPossible(selectedFile); + PluginsSettingsFilePath = DataLocation.ConvertToRelativePathIfPossible(selectedFile); } else { diff --git a/Flow.Launcher.Infrastructure/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs index fd153864636..13da9f79f3b 100644 --- a/Flow.Launcher.Infrastructure/Constant.cs +++ b/Flow.Launcher.Infrastructure/Constant.cs @@ -56,66 +56,5 @@ public static class Constant public const string Docs = "https://flowlauncher.com/docs"; public const string SystemLanguageCode = "system"; - - /// - /// Resolves a path that may be relative to an absolute path. - /// If the path is already absolute, returns it as-is. - /// If the path is relative (starts with . or doesn't contain a drive), resolves it relative to ProgramDirectory. - /// - /// The path to resolve - /// An absolute path - public static string ResolveAbsolutePath(string path) - { - if (string.IsNullOrEmpty(path)) - return path; - - // If already absolute, return as-is - if (Path.IsPathRooted(path)) - return path; - - // Resolve relative to ProgramDirectory - return Path.GetFullPath(Path.Combine(ProgramDirectory, path)); - } - - /// - /// Converts an absolute path to a relative path if it's within ProgramDirectory. - /// This enables portability by storing paths relative to the program directory when possible. - /// - /// The absolute path to convert - /// A relative path if the path is within ProgramDirectory, otherwise the original absolute path - public static string ConvertToRelativePathIfPossible(string absolutePath) - { - if (string.IsNullOrEmpty(absolutePath)) - return absolutePath; - - if (!Path.IsPathRooted(absolutePath)) - return absolutePath; - - try - { - // Get the full absolute paths for comparison - var fullAbsolutePath = Path.GetFullPath(absolutePath); - var fullProgramDir = Path.GetFullPath(ProgramDirectory); - - // Check if the absolute path is within ProgramDirectory - if (fullAbsolutePath.StartsWith(fullProgramDir, StringComparison.OrdinalIgnoreCase)) - { - // Convert to relative path - var relativePath = Path.GetRelativePath(fullProgramDir, fullAbsolutePath); - - // Prefix with .\ for clarity - if (!relativePath.StartsWith(".")) - relativePath = ".\\" + relativePath; - - return relativePath; - } - } - catch - { - // If conversion fails, return the original path - } - - return absolutePath; - } } } diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 82f83b11ac2..4588a9e6a31 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -42,5 +42,66 @@ public static bool PortableDataLocationInUse() public const string PluginEnvironments = "Environments"; public const string PluginDeleteFile = "NeedDelete.txt"; public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments); + + /// + /// Resolves a path that may be relative to an absolute path. + /// If the path is already absolute, returns it as-is. + /// If the path is relative (starts with . or doesn't contain a drive), resolves it relative to ProgramDirectory. + /// + /// The path to resolve + /// An absolute path + public static string ResolveAbsolutePath(string path) + { + if (string.IsNullOrEmpty(path)) + return path; + + // If already absolute, return as-is + if (Path.IsPathRooted(path)) + return path; + + // Resolve relative to ProgramDirectory + return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); + } + + /// + /// Converts an absolute path to a relative path if it's within ProgramDirectory. + /// This enables portability by storing paths relative to the program directory when possible. + /// + /// The absolute path to convert + /// A relative path if the path is within ProgramDirectory, otherwise the original absolute path + public static string ConvertToRelativePathIfPossible(string absolutePath) + { + if (string.IsNullOrEmpty(absolutePath)) + return absolutePath; + + if (!Path.IsPathRooted(absolutePath)) + return absolutePath; + + try + { + // Get the full absolute paths for comparison + var fullAbsolutePath = Path.GetFullPath(absolutePath); + var fullProgramDir = Path.GetFullPath(Constant.ProgramDirectory); + + // Check if the absolute path is within ProgramDirectory + if (fullAbsolutePath.StartsWith(fullProgramDir, StringComparison.OrdinalIgnoreCase)) + { + // Convert to relative path + var relativePath = Path.GetRelativePath(fullProgramDir, fullAbsolutePath); + + // Prefix with .\ for clarity + if (!relativePath.StartsWith('.')) + relativePath = ".\\" + relativePath; + + return relativePath; + } + } + catch + { + // If conversion fails, return the original path + } + + return absolutePath; + } } } From d49e5b4c1f2078412e747d6e93f4d57fab8aa860 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 24 Jan 2026 16:04:32 +0800 Subject: [PATCH 09/18] Remove unused Flow.Launcher.Infrastructure using directive Eliminated the unnecessary using statement for Flow.Launcher.Infrastructure in AbstractPluginEnvironment.cs, as its types or members are no longer referenced in this file. This helps clean up the code and avoid redundant dependencies. --- .../ExternalPlugins/Environments/AbstractPluginEnvironment.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index e08a5815e85..dead5f29306 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Windows; using System.Windows.Forms; -using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedCommands; From 9fa4c37109276b3c745f4932ac3d515da4667435 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 24 Jan 2026 16:07:04 +0800 Subject: [PATCH 10/18] Remove relative path conversion for plugin settings files Removed logic that converted absolute paths to relative paths within the ProgramDirectory. Now, plugin settings file paths are always stored as absolute paths. Deleted the ConvertToRelativePathIfPossible method and updated usages accordingly. --- .../Environments/AbstractPluginEnvironment.cs | 6 +-- .../UserSettings/DataLocation.cs | 41 ------------------- 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index dead5f29306..37a1449c1a6 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -73,8 +73,7 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { - // Convert to relative path if within ProgramDirectory for portability - PluginsSettingsFilePath = DataLocation.ConvertToRelativePathIfPossible(selectedFile); + PluginsSettingsFilePath = selectedFile; } // Nothing selected because user pressed cancel from the file dialog window else @@ -98,8 +97,7 @@ internal IEnumerable Setup() if (!string.IsNullOrEmpty(selectedFile)) { - // Convert to relative path if within ProgramDirectory for portability - PluginsSettingsFilePath = DataLocation.ConvertToRelativePathIfPossible(selectedFile); + PluginsSettingsFilePath = selectedFile; } else { diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 4588a9e6a31..2e23734d7a6 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -62,46 +62,5 @@ public static string ResolveAbsolutePath(string path) // Resolve relative to ProgramDirectory return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); } - - /// - /// Converts an absolute path to a relative path if it's within ProgramDirectory. - /// This enables portability by storing paths relative to the program directory when possible. - /// - /// The absolute path to convert - /// A relative path if the path is within ProgramDirectory, otherwise the original absolute path - public static string ConvertToRelativePathIfPossible(string absolutePath) - { - if (string.IsNullOrEmpty(absolutePath)) - return absolutePath; - - if (!Path.IsPathRooted(absolutePath)) - return absolutePath; - - try - { - // Get the full absolute paths for comparison - var fullAbsolutePath = Path.GetFullPath(absolutePath); - var fullProgramDir = Path.GetFullPath(Constant.ProgramDirectory); - - // Check if the absolute path is within ProgramDirectory - if (fullAbsolutePath.StartsWith(fullProgramDir, StringComparison.OrdinalIgnoreCase)) - { - // Convert to relative path - var relativePath = Path.GetRelativePath(fullProgramDir, fullAbsolutePath); - - // Prefix with .\ for clarity - if (!relativePath.StartsWith('.')) - relativePath = ".\\" + relativePath; - - return relativePath; - } - } - catch - { - // If conversion fails, return the original path - } - - return absolutePath; - } } } From 001101bb813f96658630b88a00e247b3e510d11f Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sat, 21 Feb 2026 20:11:49 +0800 Subject: [PATCH 11/18] Improve error handling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../UserSettings/DataLocation.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 2e23734d7a6..7b610d31a93 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -59,8 +59,19 @@ public static string ResolveAbsolutePath(string path) if (Path.IsPathRooted(path)) return path; - // Resolve relative to ProgramDirectory - return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); + // Resolve relative to ProgramDirectory, handling invalid path formats gracefully + try + { + return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); + } + catch (Exception ex) when (ex is ArgumentException || + ex is NotSupportedException || + ex is PathTooLongException) + { + // If the path cannot be resolved (invalid characters, format, or too long), + // return the original path to avoid crashing the application. + return path; + } } } } From 34a984cd86ee28313c176c79cc2c62d0e0d5a69f Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sat, 21 Feb 2026 20:12:49 +0800 Subject: [PATCH 12/18] Improve code comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 7b610d31a93..9764a7ab2dd 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -46,7 +46,7 @@ public static bool PortableDataLocationInUse() /// /// Resolves a path that may be relative to an absolute path. /// If the path is already absolute, returns it as-is. - /// If the path is relative (starts with . or doesn't contain a drive), resolves it relative to ProgramDirectory. + /// If the path is not rooted (as determined by ), resolves it relative to ProgramDirectory. /// /// The path to resolve /// An absolute path From 9b0a50376c5a9804f52206d593e5510df88a3659 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Feb 2026 20:18:09 +0800 Subject: [PATCH 13/18] Clarify exception type in path resolution catch block Updated the catch block in DataLocation.cs to explicitly use System.Exception instead of Exception when handling path resolution errors. This improves code clarity while maintaining the same error handling logic for ArgumentException, NotSupportedException, and PathTooLongException. --- Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 9764a7ab2dd..802f09492a7 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -64,7 +64,7 @@ public static string ResolveAbsolutePath(string path) { return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); } - catch (Exception ex) when (ex is ArgumentException || + catch (System.Exception ex) when (ex is ArgumentException || ex is NotSupportedException || ex is PathTooLongException) { From d62fd05599e43ddce07264faff4812ab6b8ee2a1 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Feb 2026 20:22:04 +0800 Subject: [PATCH 14/18] Ensure the path is updated in settings in case user has moved Flow to a different location --- .../ExternalPlugins/Environments/AbstractPluginEnvironment.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 37a1449c1a6..6f190bf10c9 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -61,6 +61,8 @@ internal IEnumerable Setup() if (resolvedPath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) EnsureLatestInstalled(ExecutablePath, resolvedPath, EnvPath); + // Ensure the path is updated in settings in case user has moved Flow to a different location + resolvedPath = ResolvedPluginsSettingsFilePath; return SetPathForPluginPairs(resolvedPath, Language); } From 04a56b556b4748861d41695e229a12913aef76d5 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Feb 2026 20:25:10 +0800 Subject: [PATCH 15/18] Use Path.IsPathFullyQualified for absolute path check Replaced Path.IsPathRooted with Path.IsPathFullyQualified to more accurately determine if a path is truly absolute, preventing misclassification of certain relative paths. --- Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 802f09492a7..4c3a7b417cc 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -56,7 +56,7 @@ public static string ResolveAbsolutePath(string path) return path; // If already absolute, return as-is - if (Path.IsPathRooted(path)) + if (Path.IsPathFullyQualified(path)) return path; // Resolve relative to ProgramDirectory, handling invalid path formats gracefully From 57fde0a2f31157e6b967d3b6ee1f12c658f19e4f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Feb 2026 20:30:54 +0800 Subject: [PATCH 16/18] Improve code comments --- .../ExternalPlugins/Environments/AbstractPluginEnvironment.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 6f190bf10c9..07e4c8dcbfc 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -61,7 +61,7 @@ internal IEnumerable Setup() if (resolvedPath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase)) EnsureLatestInstalled(ExecutablePath, resolvedPath, EnvPath); - // Ensure the path is updated in settings in case user has moved Flow to a different location + // Ensure the path is updated in settings in case environment was updated resolvedPath = ResolvedPluginsSettingsFilePath; return SetPathForPluginPairs(resolvedPath, Language); } @@ -112,6 +112,7 @@ internal IEnumerable Setup() InstallEnvironment(); } + // Ensure the path is updated when user has chosen to install or select environment executable resolvedPath = ResolvedPluginsSettingsFilePath; if (FilesFolders.FileExists(resolvedPath)) { From bfed5168f07bb9617f8dfbcfb193b6d398b1c671 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Sat, 21 Feb 2026 20:32:41 +0800 Subject: [PATCH 17/18] Improve code comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../ExternalPlugins/Environments/AbstractPluginEnvironment.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 07e4c8dcbfc..2c5f5502fa5 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -41,7 +41,7 @@ internal AbstractPluginEnvironment(List pluginMetadataList, Plug } /// - /// Resolves the configured plugin settings file path to an absolute path. + /// Resolves the configured runtime executable path to an absolute path. /// Supports both absolute paths and relative paths (relative to ProgramDirectory). /// private string ResolvedPluginsSettingsFilePath => DataLocation.ResolveAbsolutePath(PluginsSettingsFilePath); From f756b71207f49df654808606d5721ea74d98c17f Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Sat, 21 Feb 2026 20:34:57 +0800 Subject: [PATCH 18/18] Catch all exceptions --- Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index 4c3a7b417cc..94dda11df75 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -64,9 +64,7 @@ public static string ResolveAbsolutePath(string path) { return Path.GetFullPath(Path.Combine(Constant.ProgramDirectory, path)); } - catch (System.Exception ex) when (ex is ArgumentException || - ex is NotSupportedException || - ex is PathTooLongException) + catch (System.Exception) { // If the path cannot be resolved (invalid characters, format, or too long), // return the original path to avoid crashing the application.