Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 158 additions & 5 deletions Flow.Launcher.Test/Plugins/ShellPluginTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ 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,
leaveShellOpen,
closeShellAfterPress,
useWindowsTerminal,
runAsAdmin,
ClosePrompt);
ClosePrompt,
customConfig);

#region CMD

Expand Down Expand Up @@ -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, "")]
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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)
{
Expand All @@ -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


}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Shell/Languages/en.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,12 @@
<system:String x:Key="flowlauncher_plugin_cmd_history">Only show number of most used commands:</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_command_not_found">Command not found: {0}</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_error_running_command">Error running the command: {0}</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_error_no_exe_path_set">No executable path set. Check your shell plugin settings.</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_exe_path">Executable path</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_browse">Browse...</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_browse_filter_exe">Executable files</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_browse_filter_all">All files</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_arguments">Arguments template</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_exe_path_tooltip">Can be an absolute path or a name resolved via PATH. Example: cmd.exe</system:String>
<system:String x:Key="flowlauncher_plugin_cmd_custom_template_arguments_tooltip">Use {command} as a placeholder. Example: /k "{command}"</system:String>
</ResourceDictionary>
38 changes: 36 additions & 2 deletions Plugins/Flow.Launcher.Plugin.Shell/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -265,6 +267,13 @@ internal static ProcessStartInfo CreateProcessStartInfo(
command);
break;

case Shell.CustomTemplate:
ConfigureCustomTemplateShellStartInfo(
info,
command,
customTemplateShellConfig);
break;

default:
throw new NotImplementedException();
}
Expand Down Expand Up @@ -426,8 +435,33 @@ private static void ConfigureRunCommandStartInfo(
}
}

private static void ConfigureCustomTemplateShellStartInfo(
ProcessStartInfo info,
string command,
CustomTemplateShellConfig config)
{
if (config == null)
return;
Comment thread
DavidGBrett marked this conversation as resolved.

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<ProcessStartInfo, Process> startProcess, ProcessStartInfo info)
{
if (string.IsNullOrEmpty(info.FileName))
{
Context.API.ShowMsgError(GetTranslatedPluginTitle(),
Localize.flowlauncher_plugin_cmd_error_no_exe_path_set());
return;
Comment thread
Jack251970 marked this conversation as resolved.
}

try
{
ShellCommand.Execute(startProcess, info);
Expand Down
48 changes: 48 additions & 0 deletions Plugins/Flow.Launcher.Plugin.Shell/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,20 @@ public int ShowOnlyMostUsedCMDsNumber
}
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

private CustomTemplateShellConfig _customTemplateShellConfig = new();
public CustomTemplateShellConfig CustomTemplateShellConfig
{
get => _customTemplateShellConfig;
set
{
if (_customTemplateShellConfig != value)
{
_customTemplateShellConfig = value;
OnPropertyChanged();
}
}
}

public Dictionary<string, int> CommandHistory { get; set; } = [];

public void AddCmdHistory(string cmdName)
Expand All @@ -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
{
Expand All @@ -142,5 +187,8 @@ public enum Shell

[EnumLocalizeValue("Pwsh")]
Pwsh = 3,

[EnumLocalizeValue("Custom Template")]
CustomTemplate = 4,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> OnlyMostUsedCMDsNumbers { get; } = [5, 10, 20];
public int SelectedOnlyMostUsedCMDsNumber
{
Expand Down
Loading
Loading