diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
index 1a324a993..c37030812 100644
--- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
+++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs
@@ -4,6 +4,7 @@ using System.IO;
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 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
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 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
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 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments
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 13da9f79f..57175930f 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -56,5 +56,25 @@ namespace Flow.Launcher.Infrastructure
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 000000000..4e03a2d6f
--- /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);
+ }
+ }
+}