Skip to content

Commit

Permalink
run windows terminal plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
davidegiacometti committed Sep 22, 2021
1 parent 423faf7 commit 4c1aacb
Show file tree
Hide file tree
Showing 13 changed files with 769 additions and 0 deletions.
10 changes: 10 additions & 0 deletions PowerToys.sln
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerLauncher", "src\module
{5043CECE-E6A7-4867-9CBE-02D27D83747A} = {5043CECE-E6A7-4867-9CBE-02D27D83747A}
{F8B870EB-D5F5-45BA-9CF7-A5C459818820} = {F8B870EB-D5F5-45BA-9CF7-A5C459818820}
{74F1B9ED-F59C-4FE7-B473-7B453E30837E} = {74F1B9ED-F59C-4FE7-B473-7B453E30837E}
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} = {A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}
{4BABF3FE-3451-42FD-873F-3C332E18DCEF} = {4BABF3FE-3451-42FD-873F-3C332E18DCEF}
EndProjectSection
EndProject
Expand Down Expand Up @@ -367,6 +368,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PdfThumbnailProvider", "src
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTests-PdfThumbnailProvider", "src\modules\previewpane\UnitTests-PdfThumbnailProvider\UnitTests-PdfThumbnailProvider.csproj", "{F40C3397-1834-4530-B2D9-8F8B8456BCDF}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.PowerToys.Run.Plugin.WindowsTerminal", "src\modules\launcher\Plugins\Microsoft.PowerToys.Run.Plugin.WindowsTerminal\Microsoft.PowerToys.Run.Plugin.WindowsTerminal.csproj", "{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Expand Down Expand Up @@ -965,6 +968,12 @@ Global
{F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|x64.ActiveCfg = Release|x64
{F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|x64.Build.0 = Release|x64
{F40C3397-1834-4530-B2D9-8F8B8456BCDF}.Release|x86.ActiveCfg = Release|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|x64.ActiveCfg = Debug|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|x64.Build.0 = Debug|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Debug|x86.ActiveCfg = Debug|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|x64.ActiveCfg = Release|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|x64.Build.0 = Release|x64
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85}.Release|x86.ActiveCfg = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -1080,6 +1089,7 @@ Global
{470FBAF9-E1F8-4F3E-8786-198A1C81C8A8} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC}
{11491FD8-F921-48BF-880C-7FEA185B80A1} = {2F305555-C296-497E-AC20-5FA1B237996A}
{F40C3397-1834-4530-B2D9-8F8B8456BCDF} = {2F305555-C296-497E-AC20-5FA1B237996A}
{A2D583F0-B70C-4462-B1F0-8E81AFB7BA85} = {4AFC9975-2456-4C70-94A4-84073C1CED93}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers
{
// Application Activation Manager Class
[ComImport]
[Guid("45BA127D-10A8-46EA-8AB7-56EA9078943C")]
public class ApplicationActivationManager : IApplicationActivationManager
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)/*, PreserveSig*/]
public extern IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);

[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);

[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
public extern IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Runtime.InteropServices;

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers
{
// Reference : https://github.com/MicrosoftEdge/edge-launcher/blob/108e63df0b4cb5cd9d5e45aa7a264690851ec51d/MIcrosoftEdgeLauncherCsharp/Program.cs
[Flags]
public enum ActivateOptions
{
None = 0x00000000,
DesignMode = 0x00000001,
NoErrorUI = 0x00000002,
NoSplashScreen = 0x00000004,
}

// ApplicationActivationManager
[ComImport]
[Guid("2e941141-7f97-4756-ba1d-9decde894a3d")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IApplicationActivationManager
{
IntPtr ActivateApplication([In] string appUserModelId, [In] string arguments, [In] ActivateOptions options, [Out] out uint processId);

IntPtr ActivateForFile([In] string appUserModelId, [In] IntPtr /*IShellItemArray* */ itemArray, [In] string verb, [Out] out uint processId);

IntPtr ActivateForProtocol([In] string appUserModelId, [In] IntPtr /* IShellItemArray* */itemArray, [Out] out uint processId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers
{
public interface ITerminalQuery
{
IEnumerable<TerminalProfile> GetProfiles();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers
{
public static class TerminalHelper
{
public static string GetArguments(string profileName, bool openNewTab)
{
return openNewTab ? $"--window 0 nt --profile \"{profileName}\"" : $"--profile \"{profileName}\"";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Security.Principal;
using System.Text.Json;
using Windows.Management.Deployment;

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers
{
public class TerminalQuery : ITerminalQuery
{
private static ReadOnlyCollection<string> Packages => new List<string>
{
"Microsoft.WindowsTerminal",
"Microsoft.WindowsTerminalPreview",
}.AsReadOnly();

private static IEnumerable<TerminalPackage> _terminals;

public TerminalQuery()
{
var user = WindowsIdentity.GetCurrent().User;
var packages = new PackageManager().FindPackagesForUser(user.Value).Where(a => Packages.Contains(a.Id.Name));

var localAppDataPath = Environment.GetEnvironmentVariable("LOCALAPPDATA");

_terminals = packages.Select(p =>
{
var aumid = p.GetAppListEntries().Single().AppUserModelId;
var version = new Version(p.Id.Version.Major, p.Id.Version.Minor, p.Id.Version.Build, p.Id.Version.Revision);
var settingsPath = Path.Combine(localAppDataPath, "Packages", p.Id.FamilyName, "LocalState", "settings.json");
return new TerminalPackage(aumid, version, p.DisplayName, settingsPath);
});
}

public IEnumerable<TerminalProfile> GetProfiles()
{
var profiles = new List<TerminalProfile>();

foreach (var terminal in _terminals)
{
if (!File.Exists(terminal.SettingsPath))
{
continue;
}

var jsonText = File.ReadAllText(terminal.SettingsPath);

var options = new JsonDocumentOptions
{
CommentHandling = JsonCommentHandling.Skip,
};

var json = JsonDocument.Parse(jsonText, options);

json.RootElement.TryGetProperty("profiles", out JsonElement profilesElement);
if (profilesElement.ValueKind != JsonValueKind.Object)
{
continue;
}

profilesElement.TryGetProperty("list", out JsonElement profilesList);
if (profilesList.ValueKind != JsonValueKind.Array)
{
continue;
}

foreach (var profile in profilesList.EnumerateArray())
{
profiles.Add(ParseProfile(terminal, profile));
}
}

return profiles.OrderBy(p => p.Name);
}

private static TerminalProfile ParseProfile(TerminalPackage terminal, JsonElement profileElement)
{
profileElement.TryGetProperty("name", out JsonElement nameElement);
var name = nameElement.ValueKind == JsonValueKind.String ? nameElement.GetString() : null;

profileElement.TryGetProperty("hidden", out JsonElement hiddenElement);
var hidden = (hiddenElement.ValueKind == JsonValueKind.False || hiddenElement.ValueKind == JsonValueKind.True) && hiddenElement.GetBoolean();

profileElement.TryGetProperty("guid", out JsonElement guidElement);
var guid = guidElement.ValueKind == JsonValueKind.String ? Guid.Parse(guidElement.GetString()) : null as Guid?;

profileElement.TryGetProperty("icon", out JsonElement iconElement);
var icon = iconElement.ValueKind == JsonValueKind.String ? iconElement.GetString() : null;

return new TerminalProfile(terminal, name, guid, icon, hidden);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Helpers;
using Microsoft.PowerToys.Run.Plugin.WindowsTerminal.Properties;
using Microsoft.PowerToys.Settings.UI.Library;
using Wox.Infrastructure;
using Wox.Plugin;
using Wox.Plugin.Logger;

namespace Microsoft.PowerToys.Run.Plugin.WindowsTerminal
{
public class Main : IPlugin, IContextMenu, IPluginI18n, ISettingProvider
{
private const string OpenNewTab = nameof(OpenNewTab);
private static PluginInitContext _context;
private readonly ITerminalQuery _terminalQuery = new TerminalQuery();
private bool _openNewTab;

public string Name => Resources.plugin_name;

public string Description => Resources.plugin_description;

public IEnumerable<PluginAdditionalOption> AdditionalOptions => new List<PluginAdditionalOption>()
{
new PluginAdditionalOption()
{
Key = OpenNewTab,
DisplayLabel = Resources.open_new_tab,
Value = false,
},
};

public void Init(PluginInitContext context)
{
_context = context;
}

public List<Result> Query(Query query)
{
var search = query?.Search ?? string.Empty;
var profiles = _terminalQuery.GetProfiles();

var result = new List<Result>();

foreach (var profile in profiles)
{
// Action keyword only or search query match
if ((!string.IsNullOrWhiteSpace(query.ActionKeyword) && string.IsNullOrWhiteSpace(search)) || StringMatcher.FuzzySearch(search, profile.Name).Success)
{
result.Add(new Result
{
Title = profile.Name,
SubTitle = profile.Terminal.DisplayName,
Action = _ =>
{
Launch(profile.Terminal.AppUserModelId, profile.Name);
return true;
},
ContextData = profile,
});
}
}

return result;
}

public List<ContextMenuResult> LoadContextMenus(Result selectedResult)
{
if (!(selectedResult?.ContextData is TerminalProfile))
{
return new List<ContextMenuResult>();
}

var result = new List<ContextMenuResult>();

if (selectedResult.ContextData is TerminalProfile profile)
{
result.Add(new ContextMenuResult
{
Title = Resources.run_as_administrator,
Glyph = "\xE7EF",
FontFamily = "Segoe MDL2 Assets",
AcceleratorKey = Key.Enter,
AcceleratorModifiers = ModifierKeys.Control | ModifierKeys.Shift,
Action = _ =>
{
LaunchElevated(profile.Terminal.AppUserModelId, profile.Name);
return true;
},
});
}

return result;
}

public string GetTranslatedPluginTitle()
{
return Resources.plugin_name;
}

public string GetTranslatedPluginDescription()
{
return Resources.plugin_description;
}

private void Launch(string id, string profile)
{
var appManager = new ApplicationActivationManager();
const ActivateOptions noFlags = ActivateOptions.None;
var queryArguments = TerminalHelper.GetArguments(profile, _openNewTab);
try
{
appManager.ActivateApplication(id, queryArguments, noFlags, out var unusedPid);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
var name = "Plugin: " + Resources.plugin_name;
var message = Resources.run_terminal_failed;
Log.Exception("Failed to open Windows Terminal", ex, GetType());
_context.API.ShowMsg(name, message, string.Empty);
}
}

private void LaunchElevated(string id, string profile)
{
try
{
string path = "shell:AppsFolder\\" + id;
Helper.OpenInShell(path, TerminalHelper.GetArguments(profile, _openNewTab), runAsAdmin: true);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
var name = "Plugin: " + Resources.plugin_name;
var message = Resources.run_terminal_failed;
Log.Exception("Failed to open Windows Terminal", ex, GetType());
_context.API.ShowMsg(name, message, string.Empty);
}
}

public Control CreateSettingPanel()
{
throw new NotImplementedException();
}

public void UpdateSettings(PowerLauncherPluginSettings settings)
{
var openNewTab = false;

if (settings != null && settings.AdditionalOptions != null)
{
var optionConfirm = settings.AdditionalOptions.FirstOrDefault(x => x.Key == OpenNewTab);
openNewTab = optionConfirm?.Value ?? false;
}

_openNewTab = openNewTab;
}
}
}
Loading

1 comment on commit 4c1aacb

@github-actions
Copy link

@github-actions github-actions bot commented on 4c1aacb Sep 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@check-spelling-bot Report

Unrecognized words, please review:

  • ITerminal
Previously acknowledged words that are now absent Accessible available chrisharris coc CTriage djsoref docsmsft dogancelik estdir Fody ftps gmx htt inprivate installpowertoys itsme listbox mfreadwrite mfuuid Nefario nitroin null nunit powertoyswiki Radiobuttons sidepanel spamming systray ulazy windevbuildagents winstore xia XSmall xunit
Some files were were automatically ignored

These sample patterns would exclude them:

^src/modules/previewpane/UnitTests-MarkdownPreviewHandler/HelperFiles/MarkdownWithHTMLImageTag\.txt$

You should consider adding them to:

.github/actions/spell-check/excludes.txt

File matching is via Perl regular expressions.

To check these files, more of their words need to be in the dictionary than not. You can use patterns.txt to exclude portions, add items to the dictionary (e.g. by adding them to allow.txt), or fix typos.

To accept these unrecognized words as correct (and remove the previously acknowledged and now absent words), run the following commands

... in a clone of the git@github.com:microsoft/PowerToys.git repository
on the users/davidegiacometti/terminal-plugin branch:

update_files() {
perl -e '
my @expect_files=qw('".github/actions/spell-check/expect.txt"');
@ARGV=@expect_files;
my @stale=qw('"$patch_remove"');
my $re=join "|", @stale;
my $suffix=".".time();
my $previous="";
sub maybe_unlink { unlink($_[0]) if $_[0]; }
while (<>) {
if ($ARGV ne $old_argv) { maybe_unlink($previous); $previous="$ARGV$suffix"; rename($ARGV, $previous); open(ARGV_OUT, ">$ARGV"); select(ARGV_OUT); $old_argv = $ARGV; }
next if /^(?:$re)(?:(?:\r|\n)*$| .*)/; print;
}; maybe_unlink($previous);'
perl -e '
my $new_expect_file=".github/actions/spell-check/expect.txt";
use File::Path qw(make_path);
use File::Basename qw(dirname);
make_path (dirname($new_expect_file));
open FILE, q{<}, $new_expect_file; chomp(my @words = <FILE>); close FILE;
my @add=qw('"$patch_add"');
my %items; @items{@words} = @words x (1); @items{@add} = @add x (1);
@words = sort {lc($a)."-".$a cmp lc($b)."-".$b} keys %items;
open FILE, q{>}, $new_expect_file; for my $word (@words) { print FILE "$word\n" if $word =~ /\w/; };
close FILE;
system("git", "add", $new_expect_file);
'
(cat '.github/actions/spell-check/excludes.txt' - <<EOF
$should_exclude_patterns
EOF
) |grep .|
sort -f |
uniq > '.github/actions/spell-check/excludes.txt.temp' &&
mv '.github/actions/spell-check/excludes.txt.temp' '.github/actions/spell-check/excludes.txt'
}

comment_json=$(mktemp)
curl -L -s -S \
  --header "Content-Type: application/json" \
  "https://api.github.com/repos/microsoft/PowerToys/comments/56915994" > "$comment_json"
comment_body=$(mktemp)
jq -r .body < "$comment_json" > $comment_body
rm $comment_json

patch_remove=$(perl -ne 'next unless s{^</summary>(.*)</details>$}{$1}; print' < "$comment_body")
  

patch_add=$(perl -e '$/=undef;
$_=<>;
s{<details>.*}{}s;
s{^#.*}{};
s{\n##.*}{};
s{(?:^|\n)\s*\*}{}g;
s{\s+}{ }g;
print' < "$comment_body")
  

should_exclude_patterns=$(perl -e '$/=undef;
$_=<>;
exit unless s{(?:You should consider excluding directory paths|You should consider adding them to).*}{}s;
s{.*These sample patterns would exclude them:}{}s;
s{.*\`\`\`([^`]*)\`\`\`.*}{$1}m;
print' < "$comment_body" | grep . || true)

update_files
rm $comment_body
git add -u
If you see a bunch of garbage

If it relates to a ...

well-formed pattern

See if there's a pattern that would match it.

If not, try writing one and adding it to the patterns.txt file.

Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

Note that patterns can't match multiline strings.

binary-ish string

Please add a file path to the excludes.txt file instead of just accepting the garbage.

File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

Please sign in to comment.