diff --git a/Flow.Launcher.Test/Plugins/ShellPluginTest.cs b/Flow.Launcher.Test/Plugins/ShellPluginTest.cs
index 5270b7e9f20..cc126f606d1 100644
--- a/Flow.Launcher.Test/Plugins/ShellPluginTest.cs
+++ b/Flow.Launcher.Test/Plugins/ShellPluginTest.cs
@@ -18,7 +18,8 @@ private static ProcessStartInfo Create(
bool leaveShellOpen = false,
bool closeShellAfterPress = false,
bool useWindowsTerminal = false,
- bool runAsAdmin = false)
+ bool runAsAdmin = false,
+ CustomTemplateShellConfig customConfig = null)
=> Main.CreateProcessStartInfo(
command,
shell,
@@ -26,7 +27,8 @@ private static ProcessStartInfo Create(
closeShellAfterPress,
useWindowsTerminal,
runAsAdmin,
- ClosePrompt);
+ ClosePrompt,
+ customConfig);
#region CMD
@@ -292,6 +294,138 @@ public void RunCommand_QuotedExecutablePath_ExtractsFileName()
#endregion
+ #region CustomTemplate
+
+ [Test]
+ public void CustomTemplate_NullConfig_LeavesFileNameAsEmptyString()
+ {
+ var info = Create(
+ command: "notepad",
+ shell: Shell.CustomTemplate);
+
+ Assert.That(info.FileName, Is.Empty);
+ }
+
+ [TestCase("")]
+ [TestCase(" ")]
+ public void CustomTemplate_EmptyExecutablePath_LeavesFileNameAsEmptyString(string exePath)
+ {
+ var config = new CustomTemplateShellConfig { ExecutablePath = exePath };
+ var info = Create(
+ command: "notepad",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ Assert.That(info.FileName, Is.Empty);
+ }
+
+
+ [TestCase("cmd.exe", "/c \"{command}\"", "dir", "/c \"dir\"")]
+ [TestCase("powershell.exe", "-Command \"{command};\"", "Get-Process", "-Command \"Get-Process;\"")]
+ [TestCase("wsl.exe", "{command}", "ls", "ls")]
+ public void CustomTemplate_ReplacesCommandPlaceholder(
+ string exePath, string template, string command, string expectedArgs)
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = exePath,
+ ArgumentsTemplate = template
+ };
+ var info = Create(
+ command: command,
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ Assert.That(info.FileName, Is.EqualTo(exePath));
+ Assert.That(info.Arguments, Is.EqualTo(expectedArgs));
+ }
+
+ [TestCase("")]
+ [TestCase(" ")]
+ public void CustomTemplate_EmptyTemplate_DoesNotSetArguments(string template)
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = "pwsh.exe",
+ ArgumentsTemplate = template
+ };
+ var info = Create(
+ command: "Get-Process",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ Assert.That(info.FileName, Is.EqualTo("pwsh.exe"));
+ Assert.That(info.Arguments, Is.Empty);
+ }
+
+ [Test]
+ public void CustomTemplate_TrimsExecutablePath()
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = " pwsh.exe ",
+ ArgumentsTemplate = "-Command \"{command};\""
+ };
+ var info = Create(
+ command: "Get-Process",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ Assert.That(info.FileName, Is.EqualTo("pwsh.exe"));
+ }
+
+ [Test]
+ public void CustomTemplate_StripsQuotesFromExecutablePath()
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = "\"C:\\Program Files\\pwsh.exe\"",
+ ArgumentsTemplate = "-Command \"{command};\""
+ };
+ var info = Create(
+ command: "Get-Process",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ Assert.That(info.FileName, Is.EqualTo("C:\\Program Files\\pwsh.exe"));
+ }
+
+ [Test]
+ public void CustomTemplate_ExpandsEnvironmentVariablesInExecutablePath()
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = "%USERPROFILE%\\pwsh.exe",
+ ArgumentsTemplate = "-Command \"{command};\""
+ };
+ var info = Create(
+ command: "Get-Process",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ var expectedPath = Environment.ExpandEnvironmentVariables("%USERPROFILE%\\pwsh.exe");
+ Assert.That(info.FileName, Is.EqualTo(expectedPath));
+ }
+
+ [Test]
+ public void CustomTemplate_ExpandsEnvironmentVariablesInTemplate()
+ {
+ var config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = "cmd.exe",
+ ArgumentsTemplate = "/c \"%USERPROFILE%\\{command}\""
+ };
+ var info = Create(
+ command: "test.cmd",
+ shell: Shell.CustomTemplate,
+ customConfig: config);
+
+ var expectedArg = $"/c \"{Environment.ExpandEnvironmentVariables("%USERPROFILE%")}\\test.cmd\"";
+ Assert.That(info.Arguments, Is.EqualTo(expectedArg));
+ }
+
+ #endregion
+
#region Common
[TestCase(false, "")]
@@ -307,6 +441,7 @@ public void SetsRunAsAdminVerb(bool runAsAdmin, string expectedVerb)
[TestCase(Shell.Powershell)]
[TestCase(Shell.Pwsh)]
[TestCase(Shell.RunCommand)]
+ [TestCase(Shell.CustomTemplate)]
public void SetsWorkingDirectory(Shell shell)
{
var info = Create(shell: shell);
@@ -319,6 +454,7 @@ public void SetsWorkingDirectory(Shell shell)
[TestCase(Shell.Powershell)]
[TestCase(Shell.Pwsh)]
[TestCase(Shell.RunCommand)]
+ [TestCase(Shell.CustomTemplate)]
public void SetsUseShellExecute(Shell shell)
{
var info = Create(shell: shell);
@@ -330,13 +466,25 @@ public void SetsUseShellExecute(Shell shell)
[TestCase(Shell.Powershell)]
[TestCase(Shell.Pwsh)]
[TestCase(Shell.RunCommand)]
+ [TestCase(Shell.CustomTemplate)]
public void ExpandsEnvironmentVariables(Shell shell)
{
+ var expandedPath = Environment.ExpandEnvironmentVariables("%USERPROFILE%\\test");
+
+ CustomTemplateShellConfig config = null;
+ if (shell == Shell.CustomTemplate)
+ {
+ config = new CustomTemplateShellConfig
+ {
+ ExecutablePath = "cmd.exe",
+ ArgumentsTemplate = "/c \"{command}\""
+ };
+ }
+
var info = Create(
command: "%USERPROFILE%\\test",
- shell: shell);
-
- var expandedPath = Environment.ExpandEnvironmentVariables("%USERPROFILE%\\test");
+ shell: shell,
+ customConfig: config);
switch (shell)
{
@@ -350,9 +498,14 @@ public void ExpandsEnvironmentVariables(Shell shell)
case Shell.RunCommand:
Assert.That(info.FileName, Is.EqualTo(expandedPath));
break;
+ case Shell.CustomTemplate:
+ Assert.That(info.Arguments, Is.EqualTo($"/c \"{expandedPath}\""));
+ break;
}
}
#endregion
+
+
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Converters/LeaveShellOpenOrCloseShellAfterPressEnabledConverter.cs b/Plugins/Flow.Launcher.Plugin.Shell/Converters/LeaveShellOpenOrCloseShellAfterPressEnabledConverter.cs
index 5649353e511..bda1216d240 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Converters/LeaveShellOpenOrCloseShellAfterPressEnabledConverter.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Converters/LeaveShellOpenOrCloseShellAfterPressEnabledConverter.cs
@@ -15,7 +15,7 @@ values[1] is not Shell shell
)
return Binding.DoNothing;
- return (!closeShellAfterPressOrLeaveShellOpen) && shell != Shell.RunCommand;
+ return (!closeShellAfterPressOrLeaveShellOpen) && shell != Shell.RunCommand && shell != Shell.CustomTemplate;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
index 2fb9c6b67be..239e4d24a5e 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
@@ -19,4 +19,12 @@
Only show number of most used commands:
Command not found: {0}
Error running the command: {0}
+ No executable path set. Check your shell plugin settings.
+ Executable path
+ Browse...
+ Executable files
+ All files
+ Arguments template
+ Can be an absolute path or a name resolved via PATH. Example: cmd.exe
+ Use {command} as a placeholder. Example: /k "{command}"
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
index c4456403469..ef5912e87e9 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
@@ -199,7 +199,8 @@ private ProcessStartInfo PrepareProcessStartInfo(string command, bool runAsAdmin
_settings.CloseShellAfterPress,
_settings.UseWindowsTerminal,
runAsAdmin,
- closePrompt);
+ closePrompt,
+ _settings.CustomTemplateShellConfig);
_settings.AddCmdHistory(command);
return info;
@@ -212,7 +213,8 @@ internal static ProcessStartInfo CreateProcessStartInfo(
bool closeShellAfterPress,
bool useWindowsTerminal,
bool runAsAdmin,
- string closePrompt)
+ string closePrompt,
+ CustomTemplateShellConfig customTemplateShellConfig = null)
{
command = command.Trim();
command = Environment.ExpandEnvironmentVariables(command);
@@ -265,6 +267,13 @@ internal static ProcessStartInfo CreateProcessStartInfo(
command);
break;
+ case Shell.CustomTemplate:
+ ConfigureCustomTemplateShellStartInfo(
+ info,
+ command,
+ customTemplateShellConfig);
+ break;
+
default:
throw new NotImplementedException();
}
@@ -426,8 +435,33 @@ private static void ConfigureRunCommandStartInfo(
}
}
+ private static void ConfigureCustomTemplateShellStartInfo(
+ ProcessStartInfo info,
+ string command,
+ CustomTemplateShellConfig config)
+ {
+ if (config == null)
+ return;
+
+ if (!string.IsNullOrWhiteSpace(config.ExecutablePath))
+ info.FileName = Environment.ExpandEnvironmentVariables(config.ExecutablePath).Trim().Trim('"');
+
+ if (!string.IsNullOrWhiteSpace(config.ArgumentsTemplate))
+ {
+ var template = Environment.ExpandEnvironmentVariables(config.ArgumentsTemplate).Trim();
+ info.Arguments = template.Replace("{command}", command);
+ }
+ }
+
private void Execute(Func startProcess, ProcessStartInfo info)
{
+ if (string.IsNullOrEmpty(info.FileName))
+ {
+ Context.API.ShowMsgError(GetTranslatedPluginTitle(),
+ Localize.flowlauncher_plugin_cmd_error_no_exe_path_set());
+ return;
+ }
+
try
{
ShellCommand.Execute(startProcess, info);
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs b/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
index 79a9065347d..ddbb740e9f1 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
@@ -117,6 +117,20 @@ public int ShowOnlyMostUsedCMDsNumber
}
}
+ private CustomTemplateShellConfig _customTemplateShellConfig = new();
+ public CustomTemplateShellConfig CustomTemplateShellConfig
+ {
+ get => _customTemplateShellConfig;
+ set
+ {
+ if (_customTemplateShellConfig != value)
+ {
+ _customTemplateShellConfig = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
public Dictionary CommandHistory { get; set; } = [];
public void AddCmdHistory(string cmdName)
@@ -128,6 +142,37 @@ public void AddCmdHistory(string cmdName)
}
}
+ public class CustomTemplateShellConfig : BaseModel
+ {
+ private string _executablePath = "cmd.exe";
+ public string ExecutablePath
+ {
+ get => _executablePath;
+ set
+ {
+ if (_executablePath != value)
+ {
+ _executablePath = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ private string _argumentsTemplate = "/k \"{command}\"";
+ public string ArgumentsTemplate
+ {
+ get => _argumentsTemplate;
+ set
+ {
+ if (_argumentsTemplate != value)
+ {
+ _argumentsTemplate = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+ }
+
[EnumLocalize]
public enum Shell
{
@@ -142,5 +187,8 @@ public enum Shell
[EnumLocalizeValue("Pwsh")]
Pwsh = 3,
+
+ [EnumLocalizeValue("Custom Template")]
+ CustomTemplate = 4,
}
}
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/ViewModels/ShellSettingViewModel.cs b/Plugins/Flow.Launcher.Plugin.Shell/ViewModels/ShellSettingViewModel.cs
index 341fc386803..9bcc9b45c5f 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/ViewModels/ShellSettingViewModel.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/ViewModels/ShellSettingViewModel.cs
@@ -17,10 +17,16 @@ public Shell SelectedShell
{
Settings.Shell = value;
OnPropertyChanged();
+ OnPropertyChanged(nameof(IsCustomTemplateShell));
+ OnPropertyChanged(nameof(IsWindowsTerminalEnabled));
}
}
}
+ public bool IsCustomTemplateShell => SelectedShell == Shell.CustomTemplate;
+
+ public bool IsWindowsTerminalEnabled => SelectedShell != Shell.CustomTemplate && SelectedShell != Shell.RunCommand;
+
public List OnlyMostUsedCMDsNumbers { get; } = [5, 10, 20];
public int SelectedOnlyMostUsedCMDsNumber
{
diff --git a/Plugins/Flow.Launcher.Plugin.Shell/Views/ShellSetting.xaml b/Plugins/Flow.Launcher.Plugin.Shell/Views/ShellSetting.xaml
index 5f66884ea1b..72a85c96c8f 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Views/ShellSetting.xaml
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Views/ShellSetting.xaml
@@ -23,6 +23,8 @@
+
+
+ IsChecked="{Binding Settings.UseWindowsTerminal, Mode=TwoWay}"
+ IsEnabled="{Binding IsWindowsTerminalEnabled}" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+