Main.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Collections.Generic;
  7  using System.ComponentModel;
  8  using System.Diagnostics;
  9  using System.Globalization;
 10  using System.IO;
 11  using System.IO.Abstractions;
 12  using System.Linq;
 13  using System.Reflection;
 14  using System.Text;
 15  using System.Windows.Input;
 16  
 17  using ManagedCommon;
 18  using Microsoft.Plugin.Shell.Properties;
 19  using Microsoft.PowerToys.Settings.UI.Library;
 20  using Wox.Infrastructure.Storage;
 21  using Wox.Plugin;
 22  using Wox.Plugin.Common;
 23  using Wox.Plugin.Logger;
 24  
 25  using Control = System.Windows.Controls.Control;
 26  
 27  namespace Microsoft.Plugin.Shell
 28  {
 29      public class Main : IPlugin, IPluginI18n, ISettingProvider, IContextMenu, ISavable
 30      {
 31          private static readonly IFileSystem FileSystem = new FileSystem();
 32          private static readonly IPath Path = FileSystem.Path;
 33          private static readonly IFile File = FileSystem.File;
 34          private static readonly IDirectory Directory = FileSystem.Directory;
 35  
 36          private readonly ShellPluginSettings _settings;
 37          private readonly PluginJsonStorage<ShellPluginSettings> _storage;
 38  
 39          private static readonly CompositeFormat WoxPluginCmdCmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.wox_plugin_cmd_cmd_has_been_executed_times);
 40  
 41          private string IconPath { get; set; }
 42  
 43          public string Name => Properties.Resources.wox_plugin_cmd_plugin_name;
 44  
 45          public string Description => Properties.Resources.wox_plugin_cmd_plugin_description;
 46  
 47          public static string PluginID => "D409510CD0D2481F853690A07E6DC426";
 48  
 49          public IEnumerable<PluginAdditionalOption> AdditionalOptions => new List<PluginAdditionalOption>()
 50          {
 51              new PluginAdditionalOption()
 52              {
 53                  Key = "ShellCommandExecution",
 54                  DisplayLabel = Resources.wox_shell_command_execution,
 55                  DisplayDescription = Resources.wox_shell_command_execution_description,
 56                  PluginOptionType = PluginAdditionalOption.AdditionalOptionType.Combobox,
 57                  ComboBoxItems = new List<KeyValuePair<string, string>>
 58                  {
 59                      new KeyValuePair<string, string>(Resources.find_executable_file_and_run_it, "2"),
 60                      new KeyValuePair<string, string>(Resources.run_command_in_command_prompt, "0"),
 61                      new KeyValuePair<string, string>(Resources.run_command_in_powershell, "1"),
 62                      new KeyValuePair<string, string>(Resources.run_command_in_powershell_seven, "6"),
 63                      new KeyValuePair<string, string>(Resources.run_command_in_windows_terminal_cmd, "5"),
 64                      new KeyValuePair<string, string>(Resources.run_command_in_windows_terminal_powershell, "3"),
 65                      new KeyValuePair<string, string>(Resources.run_command_in_windows_terminal_powershell_seven, "4"),
 66                  },
 67                  ComboBoxValue = (int)_settings.Shell,
 68              },
 69  
 70              new PluginAdditionalOption()
 71              {
 72                  Key = "LeaveShellOpen",
 73                  DisplayLabel = Resources.wox_leave_shell_open,
 74                  Value = _settings.LeaveShellOpen,
 75              },
 76          };
 77  
 78          private PluginInitContext _context;
 79          private static readonly char[] Separator = new[] { ' ' };
 80  
 81          public Main()
 82          {
 83              _storage = new PluginJsonStorage<ShellPluginSettings>();
 84              _settings = _storage.Load();
 85          }
 86  
 87          public void Save()
 88          {
 89              _storage.Save();
 90          }
 91  
 92          public List<Result> Query(Query query)
 93          {
 94              ArgumentNullException.ThrowIfNull(query);
 95  
 96              List<Result> results = new List<Result>();
 97              string cmd = query.Search;
 98              if (string.IsNullOrEmpty(cmd))
 99              {
100                  return ResultsFromHistory();
101              }
102              else
103              {
104                  var queryCmd = GetCurrentCmd(cmd);
105                  results.Add(queryCmd);
106                  var history = GetHistoryCmds(cmd, queryCmd);
107                  results.AddRange(history);
108  
109                  try
110                  {
111                      IEnumerable<Result> folderPluginResults = Folder.Main.GetFolderPluginResults(query);
112                      results.AddRange(folderPluginResults);
113                  }
114                  catch (Exception e)
115                  {
116                      Log.Exception($"Exception when query for <{query}>", e, GetType());
117                  }
118  
119                  return results;
120              }
121          }
122  
123          private List<Result> GetHistoryCmds(string cmd, Result result)
124          {
125              IEnumerable<Result> history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase))
126                  .OrderByDescending(o => o.Value)
127                  .Select(m =>
128                  {
129                      if (m.Key == cmd)
130                      {
131                          // Using CurrentCulture since this is user facing
132                          result.SubTitle = Properties.Resources.wox_plugin_cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, WoxPluginCmdCmdHasBeenExecutedTimes, m.Value);
133                          return null;
134                      }
135  
136                      var ret = new Result
137                      {
138                          Title = m.Key,
139  
140                          // Using CurrentCulture since this is user facing
141                          SubTitle = Properties.Resources.wox_plugin_cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, WoxPluginCmdCmdHasBeenExecutedTimes, m.Value),
142                          IcoPath = IconPath,
143                          Action = c =>
144                          {
145                              Execute(Process.Start, PrepareProcessStartInfo(m.Key));
146                              return true;
147                          },
148                      };
149                      return ret;
150                  }).Where(o => o != null).Take(4);
151              return history.ToList();
152          }
153  
154          private Result GetCurrentCmd(string cmd)
155          {
156              Result result = new Result
157              {
158                  Title = cmd,
159                  Score = 5000,
160                  SubTitle = Properties.Resources.wox_plugin_cmd_plugin_name + ": " + Properties.Resources.wox_plugin_cmd_execute_through_shell,
161                  IcoPath = IconPath,
162                  Action = c =>
163                  {
164                      Execute(Process.Start, PrepareProcessStartInfo(cmd));
165                      return true;
166                  },
167              };
168  
169              return result;
170          }
171  
172          private List<Result> ResultsFromHistory()
173          {
174              IEnumerable<Result> history = _settings.Count.OrderByDescending(o => o.Value)
175                  .Select(m => new Result
176                  {
177                      Title = m.Key,
178  
179                      // Using CurrentCulture since this is user facing
180                      SubTitle = Properties.Resources.wox_plugin_cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, WoxPluginCmdCmdHasBeenExecutedTimes, m.Value),
181                      IcoPath = IconPath,
182                      Action = c =>
183                      {
184                          Execute(Process.Start, PrepareProcessStartInfo(m.Key));
185                          return true;
186                      },
187                  }).Take(5);
188              return history.ToList();
189          }
190  
191          private ProcessStartInfo PrepareProcessStartInfo(string command, RunAsType runAs = RunAsType.None)
192          {
193              string trimmedCommand = command.Trim();
194              command = Environment.ExpandEnvironmentVariables(trimmedCommand);
195              var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
196  
197              // Set runAsArg
198              string runAsVerbArg = string.Empty;
199              if (runAs == RunAsType.OtherUser)
200              {
201                  runAsVerbArg = "runAsUser";
202              }
203              else if (runAs == RunAsType.Administrator || _settings.RunAsAdministrator)
204              {
205                  runAsVerbArg = "runAs";
206              }
207  
208              ProcessStartInfo info;
209              if (_settings.Shell == ExecutionShell.Cmd)
210              {
211                  var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause";
212  
213                  info = ShellCommand.SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsVerbArg);
214              }
215              else if (_settings.Shell == ExecutionShell.Powershell)
216              {
217                  string arguments;
218                  if (_settings.LeaveShellOpen)
219                  {
220                      arguments = $"-NoExit \"{command}\"";
221                  }
222                  else
223                  {
224                      arguments = $"\"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
225                  }
226  
227                  info = ShellCommand.SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsVerbArg);
228              }
229              else if (_settings.Shell == ExecutionShell.PowerShellSeven)
230              {
231                  string arguments;
232                  if (_settings.LeaveShellOpen)
233                  {
234                      arguments = $"-NoExit -C \"{command}\"";
235                  }
236                  else
237                  {
238                      arguments = $"-C \"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
239                  }
240  
241                  info = ShellCommand.SetProcessStartInfo("pwsh.exe", workingDirectory, arguments, runAsVerbArg);
242              }
243              else if (_settings.Shell == ExecutionShell.WindowsTerminalCmd)
244              {
245                  string arguments;
246                  if (_settings.LeaveShellOpen)
247                  {
248                      arguments = $"cmd.exe /k \"{command}\"";
249                  }
250                  else
251                  {
252                      arguments = $"cmd.exe /c \"{command}\" & pause";
253                  }
254  
255                  info = ShellCommand.SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
256              }
257              else if (_settings.Shell == ExecutionShell.WindowsTerminalPowerShell)
258              {
259                  string arguments;
260                  if (_settings.LeaveShellOpen)
261                  {
262                      arguments = $"powershell -NoExit -C \"{command}\"";
263                  }
264                  else
265                  {
266                      arguments = $"powershell -C \"{command}\"";
267                  }
268  
269                  info = ShellCommand.SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
270              }
271              else if (_settings.Shell == ExecutionShell.WindowsTerminalPowerShellSeven)
272              {
273                  string arguments;
274                  if (_settings.LeaveShellOpen)
275                  {
276                      arguments = $"pwsh.exe -NoExit -C \"{command}\"";
277                  }
278                  else
279                  {
280                      arguments = $"pwsh.exe -C \"{command}\"";
281                  }
282  
283                  info = ShellCommand.SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
284              }
285              else if (_settings.Shell == ExecutionShell.RunCommand)
286              {
287                  // Open explorer if the path is a file or directory
288                  if (Directory.Exists(command) || File.Exists(command))
289                  {
290                      info = ShellCommand.SetProcessStartInfo("explorer.exe", arguments: command, verb: runAsVerbArg);
291                  }
292                  else
293                  {
294                      var parts = command.Split(Separator, 2);
295                      if (parts.Length == 2)
296                      {
297                          var filename = parts[0];
298                          if (ExistInPath(filename))
299                          {
300                              var arguments = parts[1];
301                              if (_settings.LeaveShellOpen)
302                              {
303                                  // Wrap the command in a cmd.exe process
304                                  info = ShellCommand.SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{filename} {arguments}\"", runAsVerbArg);
305                              }
306                              else
307                              {
308                                  info = ShellCommand.SetProcessStartInfo(filename, workingDirectory, arguments, runAsVerbArg);
309                              }
310                          }
311                          else
312                          {
313                              if (_settings.LeaveShellOpen)
314                              {
315                                  // Wrap the command in a cmd.exe process
316                                  info = ShellCommand.SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
317                              }
318                              else
319                              {
320                                  info = ShellCommand.SetProcessStartInfo(command, verb: runAsVerbArg);
321                              }
322                          }
323                      }
324                      else
325                      {
326                          if (_settings.LeaveShellOpen)
327                          {
328                              // Wrap the command in a cmd.exe process
329                              info = ShellCommand.SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
330                          }
331                          else
332                          {
333                              info = ShellCommand.SetProcessStartInfo(command, verb: runAsVerbArg);
334                          }
335                      }
336                  }
337              }
338              else
339              {
340                  throw new NotImplementedException();
341              }
342  
343              info.UseShellExecute = true;
344  
345              _settings.AddCmdHistory(trimmedCommand);
346  
347              return info;
348          }
349  
350          private enum RunAsType
351          {
352              None,
353              Administrator,
354              OtherUser,
355          }
356  
357          private void Execute(Func<ProcessStartInfo, Process> startProcess, ProcessStartInfo info)
358          {
359              try
360              {
361                  startProcess(info);
362              }
363              catch (FileNotFoundException e)
364              {
365                  var name = "Plugin: " + Properties.Resources.wox_plugin_cmd_plugin_name;
366                  var message = $"{Properties.Resources.wox_plugin_cmd_command_not_found}: {e.Message}";
367                  _context.API.ShowMsg(name, message);
368              }
369              catch (Win32Exception e)
370              {
371                  var name = "Plugin: " + Properties.Resources.wox_plugin_cmd_plugin_name;
372                  var message = $"{Properties.Resources.wox_plugin_cmd_command_failed}: {e.Message}";
373                  _context.API.ShowMsg(name, message);
374              }
375          }
376  
377          private static bool ExistInPath(string filename)
378          {
379              if (File.Exists(filename))
380              {
381                  return true;
382              }
383              else
384              {
385                  var values = Environment.GetEnvironmentVariable("PATH");
386                  if (values != null)
387                  {
388                      foreach (var path in values.Split(';'))
389                      {
390                          var path1 = Path.Combine(path, filename);
391                          var path2 = Path.Combine(path, filename + ".exe");
392                          if (File.Exists(path1) || File.Exists(path2))
393                          {
394                              return true;
395                          }
396                      }
397  
398                      return false;
399                  }
400                  else
401                  {
402                      return false;
403                  }
404              }
405          }
406  
407          public void Init(PluginInitContext context)
408          {
409              this._context = context;
410              _context.API.ThemeChanged += OnThemeChanged;
411              UpdateIconPath(_context.API.GetCurrentTheme());
412          }
413  
414          // Todo : Update with theme based IconPath
415          private void UpdateIconPath(Theme theme)
416          {
417              if (theme == Theme.Light || theme == Theme.HighContrastWhite)
418              {
419                  IconPath = "Images/shell.light.png";
420              }
421              else
422              {
423                  IconPath = "Images/shell.dark.png";
424              }
425          }
426  
427          private void OnThemeChanged(Theme currentTheme, Theme newTheme)
428          {
429              UpdateIconPath(newTheme);
430          }
431  
432          public Control CreateSettingPanel()
433          {
434              throw new NotImplementedException();
435          }
436  
437          public string GetTranslatedPluginTitle()
438          {
439              return Properties.Resources.wox_plugin_cmd_plugin_name;
440          }
441  
442          public string GetTranslatedPluginDescription()
443          {
444              return Properties.Resources.wox_plugin_cmd_plugin_description;
445          }
446  
447          public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
448          {
449              var resultList = new List<ContextMenuResult>
450              {
451                  new ContextMenuResult
452                  {
453                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
454                      Title = Properties.Resources.wox_plugin_cmd_run_as_administrator,
455                      Glyph = "\xE7EF",
456                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
457                      AcceleratorKey = Key.Enter,
458                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
459                      Action = c =>
460                      {
461                          Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, RunAsType.Administrator));
462                          return true;
463                      },
464                  },
465                  new ContextMenuResult
466                  {
467                      PluginName = Assembly.GetExecutingAssembly().GetName().Name,
468                      Title = Properties.Resources.wox_plugin_cmd_run_as_user,
469                      Glyph = "\xE7EE",
470                      FontFamily = "Segoe Fluent Icons,Segoe MDL2 Assets",
471                      AcceleratorKey = Key.U,
472                      AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
473                      Action = _ =>
474                      {
475                          Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, RunAsType.OtherUser));
476                          return true;
477                      },
478                  },
479              };
480  
481              return resultList;
482          }
483  
484          public void UpdateSettings(PowerLauncherPluginSettings settings)
485          {
486              var leaveShellOpen = false;
487              var shellOption = 2;
488  
489              if (settings != null && settings.AdditionalOptions != null)
490              {
491                  var optionLeaveShellOpen = settings.AdditionalOptions.FirstOrDefault(x => x.Key == "LeaveShellOpen");
492                  leaveShellOpen = optionLeaveShellOpen?.Value ?? leaveShellOpen;
493                  _settings.LeaveShellOpen = leaveShellOpen;
494  
495                  var optionShell = settings.AdditionalOptions.FirstOrDefault(x => x.Key == "ShellCommandExecution");
496                  shellOption = optionShell?.ComboBoxValue ?? shellOption;
497                  _settings.Shell = (ExecutionShell)shellOption;
498              }
499  
500              Save();
501          }
502      }
503  }