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}" /> - + + + + + + + + +