Add the Command Palette module (#37908)
Windows Command Palette ("CmdPal") is the next iteration of PowerToys Run. With extensibility at its core, the Command Palette is your one-stop launcher to start _anything_.
By default, CmdPal is bound to <kbd>Win+Alt+Space</kbd>.


----
This brings the current preview version of CmdPal into the upstream PowerToys repo. There are still lots of bugs to work out, but it's reached the state we're ready to start sharing it with the world. From here, we can further collaborate with the community on the features that are important, and ensuring that we've got a most robust API to enable developers to build whatever extensions they want.
Most of the built-in PT Run modules have already been ported to CmdPal's extension API. Those include:
* Installed apps
* Shell commands
* File search (powered by the indexer)
* Windows Registry search
* Web search
* Windows Terminal Profiles
* Windows Services
* Windows settings
There are a couple new extensions built-in
* You can now search for packages on `winget` and install them right from the palette. This also powers searching for extensions for the palette
* The calculator has an entirely new implementation. This is currently less feature complete than the original PT Run one - we're looking forward to updating it to be more complete for future ingestion in Windows
* "Bookmarks" allow you to save shortcuts to files, folders, and webpages as top-level commands in the palette.
We've got a bunch of other samples too, in this repo and elsewhere
### PowerToys specific notes
CmdPal will eventually graduate out of PowerToys to live as its own application, which is why it's implemented just a little differently than most other modules. Enabling CmdPal will install its `msix` package.
The CI was minorly changed to support CmdPal version numbers independent of PowerToys itself. It doesn't make sense for us to start CmdPal at v0.90, and in the future, we want to be able to rev CmdPal independently of PT itself.
Closes #3200, closes #3600, closes #7770, closes #34273, closes #36471, closes #20976, closes #14495
-----
TODOs et al
**Blocking:**
- [ ] Images and descriptions in Settings and OOBE need to be properly defined, as mentioned before
- [ ] Niels is on it
- [x] Doesn't start properly from PowerToys unless the fix PR is merged.
- https://github.com/zadjii-msft/PowerToys/pull/556 merged
- [x] I seem to lose focus a lot when I press on some limits, like between the search bar and the results.
- This is https://github.com/zadjii-msft/PowerToys/issues/427
- [x] Turned off an extension like Calculator and it was still working.
- Need to get rid of that toggle, it doesn't do anything currently
- [x] `ListViewModel.<FetchItems>` crash
- Pretty confident that was fixed in https://github.com/zadjii-msft/PowerToys/pull/553
**Not blocking / improvements:**
- Show the shortcut through settings, as mentioned before, or create a button that would open CmdPalette settings.
- When PowerToys starts, CmdPalette is always shown if enabled. That's weird when just starting PowerToys/ logging in to the computer with PowerToys auto-start activated. I think this should at least be a setting.
- Needing to double press a result for it to do the default action seems quirky. If one is already selected, I think just pressing should be enough for it to do the action.
- This is currently a setting, though we're thinking of changing the setting even more: https://github.com/zadjii-msft/PowerToys/issues/392
- There's no URI extension. Was surprised when typing a URL that it only proposed a web search.
- [x] There's no System commands extension. Was expecting to be able to quickly restart the computer by typing restart but it wasn't there.
- This is in PR https://github.com/zadjii-msft/PowerToys/pull/452
---------
Co-authored-by: joadoumie <98557455+joadoumie@users.noreply.github.com>
Co-authored-by: Jordi Adoumie <jordiadoumie@microsoft.com>
Co-authored-by: Mike Griese <zadjii@gmail.com>
Co-authored-by: Niels Laute <niels.laute@live.nl>
Co-authored-by: Michael Hawker <24302614+michael-hawker@users.noreply.github.com>
Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com>
Co-authored-by: Seraphima <zykovas91@gmail.com>
Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
Co-authored-by: Kristen Schau <47155823+krschau@users.noreply.github.com>
Co-authored-by: Eric Johnson <ericjohnson327@gmail.com>
Co-authored-by: Ethan Fang <ethanfang@microsoft.com>
Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Clint Rutkas <clint@rutkas.com>
@@ -0,0 +1,59 @@
|
||||
// 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.Linq;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public partial class AllAppsCommandProvider : CommandProvider
|
||||
{
|
||||
public static readonly AllAppsPage Page = new();
|
||||
|
||||
private readonly CommandItem _listItem;
|
||||
|
||||
public AllAppsCommandProvider()
|
||||
{
|
||||
Id = "AllApps";
|
||||
DisplayName = Resources.installed_apps;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
|
||||
Settings = AllAppsSettings.Instance.Settings;
|
||||
|
||||
_listItem = new(Page) { Subtitle = Resources.search_installed_apps };
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem];
|
||||
|
||||
public ICommandItem? LookupApp(string displayName)
|
||||
{
|
||||
var items = Page.GetItems();
|
||||
|
||||
// We're going to do this search in two directions:
|
||||
// First, is this name a substring of any app...
|
||||
var nameMatches = items.Where(i => i.Title.Contains(displayName));
|
||||
|
||||
// ... Then, does any app have this name as a substring ...
|
||||
// Only get one of these - "Terminal Preview" contains both "Terminal" and "Terminal Preview", so just take the best one
|
||||
var appMatches = items.Where(i => displayName.Contains(i.Title)).OrderByDescending(i => i.Title.Length).Take(1);
|
||||
|
||||
// ... Now, combine those two
|
||||
var both = nameMatches.Concat(appMatches);
|
||||
|
||||
if (both.Count() == 1)
|
||||
{
|
||||
return both.First();
|
||||
}
|
||||
else if (nameMatches.Count() == 1 && appMatches.Count() == 1)
|
||||
{
|
||||
if (nameMatches.First() == appMatches.First())
|
||||
{
|
||||
return nameMatches.First();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
119
src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public sealed partial class AllAppsPage : ListPage
|
||||
{
|
||||
private readonly Lock _listLock = new();
|
||||
private AppListItem[] allAppsSection = [];
|
||||
|
||||
public AllAppsPage()
|
||||
{
|
||||
this.Name = Resources.all_apps;
|
||||
this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
|
||||
this.ShowDetails = true;
|
||||
this.IsLoading = true;
|
||||
this.PlaceholderText = Resources.search_installed_apps_placeholder;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
lock (_listLock)
|
||||
{
|
||||
BuildListItems();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload())
|
||||
{
|
||||
lock (_listLock)
|
||||
{
|
||||
BuildListItems();
|
||||
}
|
||||
}
|
||||
|
||||
return allAppsSection;
|
||||
}
|
||||
|
||||
private void BuildListItems()
|
||||
{
|
||||
this.IsLoading = true;
|
||||
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
List<AppItem> apps = GetPrograms();
|
||||
|
||||
this.allAppsSection = apps
|
||||
.Select((app) => new AppListItem(app, true))
|
||||
.ToArray();
|
||||
|
||||
this.IsLoading = false;
|
||||
|
||||
AppCache.Instance.Value.ResetReloadFlag();
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
|
||||
}
|
||||
|
||||
internal List<AppItem> GetPrograms()
|
||||
{
|
||||
IEnumerable<AppItem> uwpResults = AppCache.Instance.Value.UWPs
|
||||
.Where((application) => application.Enabled)
|
||||
.Select(app =>
|
||||
new AppItem()
|
||||
{
|
||||
Name = app.Name,
|
||||
Subtitle = app.Description,
|
||||
Type = UWPApplication.Type(),
|
||||
IcoPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty,
|
||||
DirPath = app.Location,
|
||||
UserModelId = app.UserModelId,
|
||||
IsPackaged = true,
|
||||
Commands = app.GetCommands(),
|
||||
});
|
||||
|
||||
IEnumerable<AppItem> win32Results = AppCache.Instance.Value.Win32s
|
||||
.Where((application) => application.Enabled && application.Valid)
|
||||
.Select(app =>
|
||||
{
|
||||
string icoPath = string.IsNullOrEmpty(app.IcoPath) ?
|
||||
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
|
||||
app.IcoPath :
|
||||
app.FullPath) :
|
||||
app.IcoPath;
|
||||
|
||||
// icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath;
|
||||
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
|
||||
app.FullPath :
|
||||
icoPath;
|
||||
return new AppItem()
|
||||
{
|
||||
Name = app.Name,
|
||||
Subtitle = app.Description,
|
||||
Type = app.Type(),
|
||||
IcoPath = icoPath,
|
||||
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
|
||||
DirPath = app.Location,
|
||||
Commands = app.GetCommands(),
|
||||
};
|
||||
});
|
||||
|
||||
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Storage;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public sealed class AppCache : IDisposable
|
||||
{
|
||||
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
|
||||
|
||||
private PackageRepository _packageRepository;
|
||||
|
||||
private Win32ProgramRepository _win32ProgramRepository;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public IList<Win32Program> Win32s => _win32ProgramRepository.Items;
|
||||
|
||||
public IList<UWPApplication> UWPs => _packageRepository.Items;
|
||||
|
||||
public static readonly Lazy<AppCache> Instance = new(() => new());
|
||||
|
||||
public AppCache()
|
||||
{
|
||||
_win32ProgramRepositoryHelper = new Win32ProgramFileSystemWatchers();
|
||||
_win32ProgramRepository = new Win32ProgramRepository(_win32ProgramRepositoryHelper.FileSystemWatchers.Cast<IFileSystemWatcherWrapper>().ToList(), AllAppsSettings.Instance, _win32ProgramRepositoryHelper.PathsToWatch);
|
||||
|
||||
_packageRepository = new PackageRepository(new PackageCatalogWrapper());
|
||||
|
||||
var a = Task.Run(() =>
|
||||
{
|
||||
_win32ProgramRepository.IndexPrograms();
|
||||
});
|
||||
|
||||
var b = Task.Run(() =>
|
||||
{
|
||||
_packageRepository.IndexPrograms();
|
||||
UpdateUWPIconPath(ThemeHelper.GetCurrentTheme());
|
||||
});
|
||||
|
||||
Task.WaitAll(a, b);
|
||||
|
||||
AllAppsSettings.Instance.LastIndexTime = DateTime.Today;
|
||||
}
|
||||
|
||||
private void UpdateUWPIconPath(Theme theme)
|
||||
{
|
||||
if (_packageRepository != null)
|
||||
{
|
||||
foreach (UWPApplication app in _packageRepository)
|
||||
{
|
||||
app.UpdateLogoPath(theme);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool ShouldReload() => _packageRepository.ShouldReload() || _win32ProgramRepository.ShouldReload();
|
||||
|
||||
public void ResetReloadFlag()
|
||||
{
|
||||
_packageRepository.ResetReloadFlag();
|
||||
_win32ProgramRepository.ResetReloadFlag();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_win32ProgramRepositoryHelper?.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppItem.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
internal sealed class AppItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Subtitle { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
public string IcoPath { get; set; } = string.Empty;
|
||||
|
||||
public string ExePath { get; set; } = string.Empty;
|
||||
|
||||
public string DirPath { get; set; } = string.Empty;
|
||||
|
||||
public string UserModelId { get; set; } = string.Empty;
|
||||
|
||||
public bool IsPackaged { get; set; }
|
||||
|
||||
public List<CommandContextItem>? Commands { get; set; }
|
||||
|
||||
public AppItem()
|
||||
{
|
||||
}
|
||||
}
|
||||
104
src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Apps/AppListItem.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
internal sealed partial class AppListItem : ListItem
|
||||
{
|
||||
private readonly AppItem _app;
|
||||
private static readonly Tag _appTag = new("App");
|
||||
|
||||
private readonly Lazy<Details> _details;
|
||||
private readonly Lazy<IconInfo> _icon;
|
||||
|
||||
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
|
||||
|
||||
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
|
||||
|
||||
public AppListItem(AppItem app, bool useThumbnails)
|
||||
: base(new AppCommand(app))
|
||||
{
|
||||
_app = app;
|
||||
Title = app.Name;
|
||||
Subtitle = app.Subtitle;
|
||||
Tags = [_appTag];
|
||||
MoreCommands = _app.Commands!.ToArray();
|
||||
|
||||
_details = new Lazy<Details>(() => BuildDetails());
|
||||
_icon = new Lazy<IconInfo>(() =>
|
||||
{
|
||||
var t = FetchIcon(useThumbnails);
|
||||
t.Wait();
|
||||
return t.Result;
|
||||
});
|
||||
}
|
||||
|
||||
private Details BuildDetails()
|
||||
{
|
||||
var metadata = new List<DetailsElement>();
|
||||
metadata.Add(new DetailsElement() { Key = "Type", Data = new DetailsTags() { Tags = [new Tag(_app.Type)] } });
|
||||
if (!_app.IsPackaged)
|
||||
{
|
||||
metadata.Add(new DetailsElement() { Key = "Path", Data = new DetailsLink() { Text = _app.ExePath } });
|
||||
}
|
||||
|
||||
return new Details()
|
||||
{
|
||||
Title = this.Title,
|
||||
HeroImage = this.Icon ?? new IconInfo(string.Empty),
|
||||
Metadata = metadata.ToArray(),
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IconInfo> FetchIcon(bool useThumbnails)
|
||||
{
|
||||
IconInfo? icon = null;
|
||||
if (_app.IsPackaged)
|
||||
{
|
||||
icon = new IconInfo(_app.IcoPath);
|
||||
if (_details.IsValueCreated)
|
||||
{
|
||||
_details.Value.HeroImage = icon;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
|
||||
if (useThumbnails)
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
|
||||
if (stream != null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
icon = new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
icon = icon ?? new IconInfo(_app.IcoPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
icon = new IconInfo(_app.IcoPath);
|
||||
}
|
||||
|
||||
if (_details.IsValueCreated)
|
||||
{
|
||||
_details.Value.HeroImage = icon;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1825_17772)">
|
||||
<path d="M7.2002 8H12.8002C13.242 8 13.6002 8.35817 13.6002 8.8V13.6C13.6002 14.0418 13.242 14.4 12.8002 14.4H7.2002V8Z" fill="url(#paint0_linear_1825_17772)"/>
|
||||
<path d="M0.799805 8H7.1998V14.4H1.59981C1.15798 14.4 0.799805 14.0418 0.799805 13.6V8Z" fill="url(#paint1_linear_1825_17772)"/>
|
||||
<path d="M0.799805 2.39961C0.799805 1.95778 1.15798 1.59961 1.5998 1.59961H6.3998C6.84163 1.59961 7.1998 1.95778 7.1998 2.39961V7.99961H0.799805V2.39961Z" fill="url(#paint2_linear_1825_17772)"/>
|
||||
<rect width="6.4" height="6.4" rx="0.8" transform="matrix(0.701061 0.713102 -0.701061 0.713102 10.7998 0)" fill="url(#paint3_linear_1825_17772)"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1825_17772" x1="12.5686" y1="15.0983" x2="7.57856" y2="7.74389" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5C6166"/>
|
||||
<stop offset="1" stop-color="#8A9299"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1825_17772" x1="6.36596" y1="14.8714" x2="1.98416" y2="7.32118" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#2E3133"/>
|
||||
<stop offset="1" stop-color="#5C6166"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1825_17772" x1="6.17254" y1="8.59747" x2="2.16557" y2="1.7111" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8A9299"/>
|
||||
<stop offset="1" stop-color="#A1AAB3"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1825_17772" x1="6.75524" y1="3.90242" x2="-0.056471" y2="3.62557" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0078D4"/>
|
||||
<stop offset="1" stop-color="#3CCBF4"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1825_17772">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1,52 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Apps</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="WyHash" />
|
||||
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\AllApps.png" />
|
||||
<None Remove="Assets\AllApps.svg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\AllApps.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\AllApps.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,19 @@
|
||||
GetPhysicallyInstalledSystemMemory
|
||||
GlobalMemoryStatusEx
|
||||
GetSystemInfo
|
||||
CoCreateInstance
|
||||
SetForegroundWindow
|
||||
IsIconic
|
||||
RegisterHotKey
|
||||
SetWindowLongPtr
|
||||
CallWindowProc
|
||||
ShowWindow
|
||||
SetForegroundWindow
|
||||
SetFocus
|
||||
SetActiveWindow
|
||||
MonitorFromWindow
|
||||
GetMonitorInfo
|
||||
SHCreateStreamOnFileEx
|
||||
CoAllowSetForegroundWindow
|
||||
SHCreateStreamOnFileEx
|
||||
SHLoadIndirectString
|
||||
@@ -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.
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
// Reference : https://stackoverflow.com/questions/32122679/getting-icon-of-modern-windows-app-from-a-desktop-application
|
||||
[Guid("5842a140-ff9f-4166-8f5c-62f5b7b0c781")]
|
||||
[ComImport]
|
||||
public class AppxFactory
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Win32.System.Com;
|
||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public static class AppxPackageHelper
|
||||
{
|
||||
private static readonly IAppxFactory AppxFactory = (IAppxFactory)new AppxFactory();
|
||||
|
||||
// This function returns a list of attributes of applications
|
||||
internal static IEnumerable<IAppxManifestApplication> GetAppsFromManifest(IStream stream)
|
||||
{
|
||||
var reader = AppxFactory.CreateManifestReader(stream);
|
||||
var manifestApps = reader.GetApplications();
|
||||
|
||||
while (manifestApps.GetHasCurrent())
|
||||
{
|
||||
var manifestApp = manifestApps.GetCurrent();
|
||||
var hr = manifestApp.GetStringValue("AppListEntry", out var appListEntry);
|
||||
_ = CheckHRAndReturnOrThrow(hr, appListEntry);
|
||||
if (appListEntry != "none")
|
||||
{
|
||||
yield return manifestApp;
|
||||
}
|
||||
|
||||
manifestApps.MoveNext();
|
||||
}
|
||||
}
|
||||
|
||||
internal static T CheckHRAndReturnOrThrow<T>(HRESULT hr, T result)
|
||||
{
|
||||
if (hr != HRESULT.S_OK)
|
||||
{
|
||||
Marshal.ThrowExceptionForHR((int)hr);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using Windows.Win32.System.Com;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Guid("BEB94909-E451-438B-B5A7-D79E767B75D8")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxFactory
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
|
||||
void _VtblGap0_2(); // skip 2 methods
|
||||
|
||||
internal IAppxManifestReader CreateManifestReader(IStream inputStream);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Guid("5DA89BF4-3773-46BE-B650-7E744863B7E8")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestApplication
|
||||
{
|
||||
[PreserveSig]
|
||||
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
|
||||
[PreserveSig]
|
||||
HRESULT GetAppUserModelId([MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Guid("9EB8A55A-F04B-4D0D-808D-686185D4847A")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestApplicationsEnumerator
|
||||
{
|
||||
IAppxManifestApplication GetCurrent();
|
||||
|
||||
bool GetHasCurrent();
|
||||
|
||||
bool MoveNext();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Guid("03FAF64D-F26F-4B2C-AAF7-8FE7789B8BCA")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestProperties
|
||||
{
|
||||
[PreserveSig]
|
||||
HRESULT GetBoolValue([MarshalAs(UnmanagedType.LPWStr)] string name, out bool value);
|
||||
|
||||
[PreserveSig]
|
||||
HRESULT GetStringValue([MarshalAs(UnmanagedType.LPWStr)] string name, [MarshalAs(UnmanagedType.LPWStr)] out string value);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Guid("4E1BD148-55A0-4480-A3D1-15544710637C")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IAppxManifestReader
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
|
||||
void _VtblGap0_1(); // skip 1 method
|
||||
|
||||
IAppxManifestProperties GetProperties();
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:Element should begin with upper-case letter", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Implements COM Interface")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Implements COM Interface")]
|
||||
void _VtblGap1_5(); // skip 5 methods
|
||||
|
||||
IAppxManifestApplicationsEnumerator GetApplications();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public interface IPackage
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
string FullName { get; }
|
||||
|
||||
string FamilyName { get; }
|
||||
|
||||
bool IsFramework { get; }
|
||||
|
||||
bool IsDevelopmentMode { get; }
|
||||
|
||||
string InstalledLocation { get; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public interface IPackageManager
|
||||
{
|
||||
IEnumerable<IPackage> FindPackagesForCurrentUser();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public interface IProgram
|
||||
{
|
||||
string UniqueIdentifier { get; set; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
string Description { get; set; }
|
||||
|
||||
string Location { get; }
|
||||
|
||||
bool Enabled { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public enum LogoType
|
||||
{
|
||||
Error,
|
||||
Colored,
|
||||
HighContrast,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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;
|
||||
using System.Linq;
|
||||
using System.Security.Principal;
|
||||
using Windows.Management.Deployment;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public class PackageManagerWrapper : IPackageManager
|
||||
{
|
||||
private readonly PackageManager _packageManager;
|
||||
|
||||
public PackageManagerWrapper()
|
||||
{
|
||||
_packageManager = new PackageManager();
|
||||
}
|
||||
|
||||
public IEnumerable<IPackage> FindPackagesForCurrentUser()
|
||||
{
|
||||
var user = WindowsIdentity.GetCurrent().User;
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
var pkgs = _packageManager.FindPackagesForUser(user.Value);
|
||||
|
||||
return pkgs.Select(PackageWrapper.GetWrapperFromPackage).Where(package => package != null);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<IPackage>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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.IO;
|
||||
using Windows.Foundation.Metadata;
|
||||
using Package = Windows.ApplicationModel.Package;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
public class PackageWrapper : IPackage
|
||||
{
|
||||
public string Name { get; } = string.Empty;
|
||||
|
||||
public string FullName { get; } = string.Empty;
|
||||
|
||||
public string FamilyName { get; } = string.Empty;
|
||||
|
||||
public bool IsFramework { get; }
|
||||
|
||||
public bool IsDevelopmentMode { get; }
|
||||
|
||||
public string InstalledLocation { get; } = string.Empty;
|
||||
|
||||
public PackageWrapper()
|
||||
{
|
||||
}
|
||||
|
||||
public PackageWrapper(string name, string fullName, string familyName, bool isFramework, bool isDevelopmentMode, string installedLocation)
|
||||
{
|
||||
Name = name;
|
||||
FullName = fullName;
|
||||
FamilyName = familyName;
|
||||
IsFramework = isFramework;
|
||||
IsDevelopmentMode = isDevelopmentMode;
|
||||
InstalledLocation = installedLocation;
|
||||
}
|
||||
|
||||
private static readonly Lazy<bool> IsPackageDotInstallationPathAvailable = new(() =>
|
||||
ApiInformation.IsPropertyPresent(typeof(Package).FullName, nameof(Package.InstalledLocation.Path)));
|
||||
|
||||
public static PackageWrapper GetWrapperFromPackage(Package package)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(package);
|
||||
|
||||
string path;
|
||||
try
|
||||
{
|
||||
path = IsPackageDotInstallationPathAvailable.Value ? GetInstalledPath(package) : package.InstalledLocation.Path;
|
||||
}
|
||||
catch (Exception e) when (e is ArgumentException || e is FileNotFoundException || e is DirectoryNotFoundException)
|
||||
{
|
||||
return new PackageWrapper(
|
||||
package.Id.Name,
|
||||
package.Id.FullName,
|
||||
package.Id.FamilyName,
|
||||
package.IsFramework,
|
||||
package.IsDevelopmentMode,
|
||||
string.Empty);
|
||||
}
|
||||
|
||||
return new PackageWrapper(
|
||||
package.Id.Name,
|
||||
package.Id.FullName,
|
||||
package.Id.FamilyName,
|
||||
package.IsFramework,
|
||||
package.IsDevelopmentMode,
|
||||
path);
|
||||
}
|
||||
|
||||
// This is a separate method so the reference to .InstalledPath won't be loaded in API versions which do not support this API (e.g. older then Build 19041)
|
||||
private static string GetInstalledPath(Package package)
|
||||
=> package.InstalledLocation.Path;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
/// <summary>
|
||||
/// Contains user added folder location contents as well as all user disabled applications
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Win32 class applications set UniqueIdentifier using their full file path</para>
|
||||
/// <para>UWP class applications set UniqueIdentifier using their Application User Model ID</para>
|
||||
/// <para>Custom user added program sources set UniqueIdentifier using their location</para>
|
||||
/// </remarks>
|
||||
public class ProgramSource
|
||||
{
|
||||
public string Location { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
public string UniqueIdentifier { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
// 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.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.System.Com;
|
||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Serializable]
|
||||
public partial class UWP
|
||||
{
|
||||
private static readonly IPath Path = new FileSystem().Path;
|
||||
|
||||
private static readonly Dictionary<string, PackageVersion> _versionFromNamespace = new()
|
||||
{
|
||||
{ "http://schemas.microsoft.com/appx/manifest/foundation/windows10", PackageVersion.Windows10 },
|
||||
{ "http://schemas.microsoft.com/appx/2013/manifest", PackageVersion.Windows81 },
|
||||
{ "http://schemas.microsoft.com/appx/2010/manifest", PackageVersion.Windows8 },
|
||||
};
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string FullName { get; }
|
||||
|
||||
public string FamilyName { get; }
|
||||
|
||||
public string Location { get; set; } = string.Empty;
|
||||
|
||||
// Localized path based on windows display language
|
||||
public string LocationLocalized { get; set; } = string.Empty;
|
||||
|
||||
public IList<UWPApplication> Apps { get; private set; } = new List<UWPApplication>();
|
||||
|
||||
public PackageVersion Version { get; set; }
|
||||
|
||||
public static IPackageManager PackageManagerWrapper { get; set; } = new PackageManagerWrapper();
|
||||
|
||||
public UWP(IPackage package)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(package);
|
||||
|
||||
Name = package.Name;
|
||||
FullName = package.FullName;
|
||||
FamilyName = package.FamilyName;
|
||||
}
|
||||
|
||||
public void InitializeAppInfo(string installedLocation)
|
||||
{
|
||||
Location = installedLocation;
|
||||
LocationLocalized = ShellLocalization.Instance.GetLocalizedPath(installedLocation);
|
||||
var path = Path.Combine(installedLocation, "AppxManifest.xml");
|
||||
|
||||
var namespaces = XmlNamespaces(path);
|
||||
InitPackageVersion(namespaces);
|
||||
|
||||
const uint noAttribute = 0x80;
|
||||
|
||||
var access = (uint)STGM.READ;
|
||||
var hResult = PInvoke.SHCreateStreamOnFileEx(path, access, noAttribute, false, null, out IStream stream);
|
||||
|
||||
// S_OK
|
||||
if (hResult == 0)
|
||||
{
|
||||
Apps = AppxPackageHelper.GetAppsFromManifest(stream).Select(appInManifest => new UWPApplication(appInManifest, this)).Where(a =>
|
||||
{
|
||||
var valid =
|
||||
!string.IsNullOrEmpty(a.UserModelId) &&
|
||||
!string.IsNullOrEmpty(a.DisplayName) &&
|
||||
a.AppListEntry != "none";
|
||||
|
||||
return valid;
|
||||
}).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
Apps = Array.Empty<UWPApplication>();
|
||||
}
|
||||
}
|
||||
|
||||
// http://www.hanselman.com/blog/GetNamespacesFromAnXMLDocumentWithXPathDocumentAndLINQToXML.aspx
|
||||
private static string[] XmlNamespaces(string path)
|
||||
{
|
||||
var z = XDocument.Load(path);
|
||||
if (z.Root != null)
|
||||
{
|
||||
var namespaces = z.Root.Attributes().
|
||||
Where(a => a.IsNamespaceDeclaration).
|
||||
GroupBy(
|
||||
a => a.Name.Namespace == XNamespace.None ? string.Empty : a.Name.LocalName,
|
||||
a => XNamespace.Get(a.Value)).Select(
|
||||
g => g.First().ToString()).ToArray();
|
||||
return namespaces;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
|
||||
private void InitPackageVersion(string[] namespaces)
|
||||
{
|
||||
foreach (var n in _versionFromNamespace.Keys.Where(namespaces.Contains))
|
||||
{
|
||||
Version = _versionFromNamespace[n];
|
||||
return;
|
||||
}
|
||||
|
||||
Version = PackageVersion.Unknown;
|
||||
}
|
||||
|
||||
public static UWPApplication[] All()
|
||||
{
|
||||
var windows10 = new Version(10, 0);
|
||||
var support = Environment.OSVersion.Version.Major >= windows10.Major;
|
||||
if (support)
|
||||
{
|
||||
var applications = CurrentUserPackages().AsParallel().SelectMany(p =>
|
||||
{
|
||||
UWP u;
|
||||
try
|
||||
{
|
||||
u = new UWP(p);
|
||||
u.InitializeAppInfo(p.InstalledLocation);
|
||||
}
|
||||
catch (Exception )
|
||||
{
|
||||
return Array.Empty<UWPApplication>();
|
||||
}
|
||||
|
||||
return u.Apps;
|
||||
});
|
||||
|
||||
var updatedListWithoutDisabledApps = applications
|
||||
.Where(t1 => AllAppsSettings.Instance.DisabledProgramSources.All(x => x.UniqueIdentifier != t1.UniqueIdentifier))
|
||||
.Select(x => x);
|
||||
|
||||
return updatedListWithoutDisabledApps.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
return Array.Empty<UWPApplication>();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<IPackage> CurrentUserPackages()
|
||||
{
|
||||
return PackageManagerWrapper.FindPackagesForCurrentUser().Where(p =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var f = p.IsFramework;
|
||||
var path = p.InstalledLocation;
|
||||
return !f && !string.IsNullOrEmpty(path);
|
||||
}
|
||||
catch (Exception )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FamilyName;
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1309:Use ordinal string comparison", Justification = "Using CurrentCultureIgnoreCase since this is used with FamilyName")]
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (obj is UWP uwp)
|
||||
{
|
||||
// Using CurrentCultureIgnoreCase since this is used with FamilyName
|
||||
return FamilyName.Equals(uwp.FamilyName, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// Using CurrentCultureIgnoreCase since this is used with FamilyName
|
||||
return FamilyName.GetHashCode(StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public enum PackageVersion
|
||||
{
|
||||
Windows10,
|
||||
Windows81,
|
||||
Windows8,
|
||||
Unknown,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
// 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.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using static Microsoft.CmdPal.Ext.Apps.Utils.Native;
|
||||
using PackageVersion = Microsoft.CmdPal.Ext.Apps.Programs.UWP.PackageVersion;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Serializable]
|
||||
public class UWPApplication : IProgram
|
||||
{
|
||||
private static readonly IFileSystem FileSystem = new FileSystem();
|
||||
private static readonly IPath Path = FileSystem.Path;
|
||||
private static readonly IFile File = FileSystem.File;
|
||||
|
||||
public string AppListEntry { get; set; } = string.Empty;
|
||||
|
||||
public string UniqueIdentifier { get; set; }
|
||||
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string UserModelId { get; set; }
|
||||
|
||||
public string BackgroundColor { get; set; }
|
||||
|
||||
public string EntryPoint { get; set; }
|
||||
|
||||
public string Name => DisplayName;
|
||||
|
||||
public string Location => Package.Location;
|
||||
|
||||
// Localized path based on windows display language
|
||||
public string LocationLocalized => Package.LocationLocalized;
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool CanRunElevated { get; set; }
|
||||
|
||||
public string LogoPath { get; set; } = string.Empty;
|
||||
|
||||
public LogoType LogoType { get; set; }
|
||||
|
||||
public UWP Package { get; set; }
|
||||
|
||||
private string logoUri;
|
||||
|
||||
private const string ContrastWhite = "contrast-white";
|
||||
|
||||
private const string ContrastBlack = "contrast-black";
|
||||
|
||||
// Function to set the subtitle based on the Type of application
|
||||
public static string Type()
|
||||
{
|
||||
return Resources.packaged_application;
|
||||
}
|
||||
|
||||
public List<CommandContextItem> GetCommands()
|
||||
{
|
||||
List<CommandContextItem> commands = new List<CommandContextItem>();
|
||||
|
||||
if (CanRunElevated)
|
||||
{
|
||||
commands.Add(
|
||||
new CommandContextItem(
|
||||
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true)));
|
||||
|
||||
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
|
||||
}
|
||||
|
||||
commands.Add(
|
||||
new CommandContextItem(
|
||||
new OpenPathCommand(Location)
|
||||
{
|
||||
Name = Resources.open_containing_folder,
|
||||
Icon = new("\ue838"),
|
||||
}));
|
||||
|
||||
commands.Add(
|
||||
new CommandContextItem(
|
||||
new OpenInConsoleCommand(Package.Location)));
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
public UWPApplication(IAppxManifestApplication manifestApp, UWP package)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manifestApp);
|
||||
|
||||
var hr = manifestApp.GetAppUserModelId(out var tmpUserModelId);
|
||||
UserModelId = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUserModelId);
|
||||
|
||||
hr = manifestApp.GetAppUserModelId(out var tmpUniqueIdentifier);
|
||||
UniqueIdentifier = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpUniqueIdentifier);
|
||||
|
||||
hr = manifestApp.GetStringValue("DisplayName", out var tmpDisplayName);
|
||||
DisplayName = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDisplayName);
|
||||
|
||||
hr = manifestApp.GetStringValue("Description", out var tmpDescription);
|
||||
Description = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpDescription);
|
||||
|
||||
hr = manifestApp.GetStringValue("BackgroundColor", out var tmpBackgroundColor);
|
||||
BackgroundColor = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpBackgroundColor);
|
||||
|
||||
hr = manifestApp.GetStringValue("EntryPoint", out var tmpEntryPoint);
|
||||
EntryPoint = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, tmpEntryPoint);
|
||||
|
||||
Package = package ?? throw new ArgumentNullException(nameof(package));
|
||||
|
||||
DisplayName = ResourceFromPri(package.FullName, DisplayName);
|
||||
Description = ResourceFromPri(package.FullName, Description);
|
||||
logoUri = LogoUriFromManifest(manifestApp);
|
||||
|
||||
Enabled = true;
|
||||
CanRunElevated = IfApplicationCanRunElevated();
|
||||
}
|
||||
|
||||
private bool IfApplicationCanRunElevated()
|
||||
{
|
||||
if (EntryPoint == "Windows.FullTrustApplication")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var manifest = Package.Location + "\\AppxManifest.xml";
|
||||
if (File.Exists(manifest))
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check the manifest to verify if the Trust Level for the application is "mediumIL"
|
||||
var file = File.ReadAllText(manifest);
|
||||
var xmlDoc = new XmlDocument();
|
||||
xmlDoc.LoadXml(file);
|
||||
var xmlRoot = xmlDoc.DocumentElement;
|
||||
var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
|
||||
namespaceManager.AddNamespace("uap10", "http://schemas.microsoft.com/appx/manifest/uap/windows10/10");
|
||||
var trustLevelNode = xmlRoot?.SelectSingleNode("//*[local-name()='Application' and @uap10:TrustLevel]", namespaceManager); // According to https://learn.microsoft.com/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps#create-a-package-manifest-for-the-sparse-package and https://learn.microsoft.com/uwp/schemas/appxpackage/uapmanifestschema/element-application#attributes
|
||||
|
||||
if (trustLevelNode?.Attributes?["uap10:TrustLevel"]?.Value == "mediumIL")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal string ResourceFromPri(string packageFullName, string resourceReference)
|
||||
{
|
||||
const string prefix = "ms-resource:";
|
||||
|
||||
// Using OrdinalIgnoreCase since this is used internally
|
||||
if (!string.IsNullOrWhiteSpace(resourceReference) && resourceReference.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// magic comes from @talynone
|
||||
// https://github.com/talynone/Wox.Plugin.WindowsUniversalAppLauncher/blob/master/StoreAppLauncher/Helpers/NativeApiHelper.cs#L139-L153
|
||||
var key = resourceReference.Substring(prefix.Length);
|
||||
string parsed;
|
||||
var parsedFallback = string.Empty;
|
||||
|
||||
// Using Ordinal/OrdinalIgnoreCase since these are used internally
|
||||
if (key.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
parsed = prefix + key;
|
||||
}
|
||||
else if (key.StartsWith('/'))
|
||||
{
|
||||
parsed = prefix + "//" + key;
|
||||
}
|
||||
else if (key.Contains("resources", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
parsed = prefix + key;
|
||||
}
|
||||
else
|
||||
{
|
||||
parsed = prefix + "///resources/" + key;
|
||||
|
||||
// e.g. for Windows Terminal version >= 1.12 DisplayName and Description resources are not in the 'resources' subtree
|
||||
parsedFallback = prefix + "///" + key;
|
||||
}
|
||||
|
||||
var outBuffer = new StringBuilder(128);
|
||||
var source = $"@{{{packageFullName}? {parsed}}}";
|
||||
var capacity = (uint)outBuffer.Capacity;
|
||||
var hResult = SHLoadIndirectString(source, outBuffer, capacity, IntPtr.Zero);
|
||||
if (hResult != HRESULT.S_OK)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(parsedFallback))
|
||||
{
|
||||
var sourceFallback = $"@{{{packageFullName}? {parsedFallback}}}";
|
||||
hResult = SHLoadIndirectString(sourceFallback, outBuffer, capacity, IntPtr.Zero);
|
||||
if (hResult == HRESULT.S_OK)
|
||||
{
|
||||
var loaded = outBuffer.ToString();
|
||||
if (!string.IsNullOrEmpty(loaded))
|
||||
{
|
||||
return loaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/Wox-launcher/Wox/issues/964
|
||||
// known hresult 2147942522:
|
||||
// 'Microsoft Corporation' violates pattern constraint of '\bms-resource:.{1,256}'.
|
||||
// for
|
||||
// Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description
|
||||
// Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription
|
||||
return string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
var loaded = outBuffer.ToString();
|
||||
if (!string.IsNullOrEmpty(loaded))
|
||||
{
|
||||
return loaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return resourceReference;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Dictionary<PackageVersion, string> _logoKeyFromVersion = new Dictionary<PackageVersion, string>
|
||||
{
|
||||
{ PackageVersion.Windows10, "Square44x44Logo" },
|
||||
{ PackageVersion.Windows81, "Square30x30Logo" },
|
||||
{ PackageVersion.Windows8, "SmallLogo" },
|
||||
};
|
||||
|
||||
internal string LogoUriFromManifest(IAppxManifestApplication app)
|
||||
{
|
||||
if (_logoKeyFromVersion.TryGetValue(Package.Version, out var key))
|
||||
{
|
||||
var hr = app.GetStringValue(key, out var logoUriFromApp);
|
||||
_ = AppxPackageHelper.CheckHRAndReturnOrThrow(hr, logoUriFromApp);
|
||||
return logoUriFromApp;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLogoPath(Theme theme)
|
||||
{
|
||||
LogoPathFromUri(logoUri, theme);
|
||||
}
|
||||
|
||||
// scale factors on win10: https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets#asset-size-tables,
|
||||
private static readonly Dictionary<PackageVersion, List<int>> _scaleFactors = new Dictionary<PackageVersion, List<int>>
|
||||
{
|
||||
{ PackageVersion.Windows10, new List<int> { 100, 125, 150, 200, 400 } },
|
||||
{ PackageVersion.Windows81, new List<int> { 100, 120, 140, 160, 180 } },
|
||||
{ PackageVersion.Windows8, new List<int> { 100 } },
|
||||
};
|
||||
|
||||
private bool SetScaleIcons(string path, string colorscheme, bool highContrast = false)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension != null)
|
||||
{
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path.Substring(0, end);
|
||||
var paths = new List<string> { };
|
||||
|
||||
if (!highContrast)
|
||||
{
|
||||
paths.Add(path);
|
||||
}
|
||||
|
||||
if (_scaleFactors.TryGetValue(Package.Version, out var factors))
|
||||
{
|
||||
foreach (var factor in factors)
|
||||
{
|
||||
if (highContrast)
|
||||
{
|
||||
paths.Add($"{prefix}.scale-{factor}_{colorscheme}{extension}");
|
||||
paths.Add($"{prefix}.{colorscheme}_scale-{factor}{extension}");
|
||||
}
|
||||
else
|
||||
{
|
||||
paths.Add($"{prefix}.scale-{factor}{extension}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIconPath = paths.FirstOrDefault(File.Exists);
|
||||
if (!string.IsNullOrEmpty(selectedIconPath))
|
||||
{
|
||||
LogoPath = selectedIconPath;
|
||||
if (highContrast)
|
||||
{
|
||||
LogoType = LogoType.HighContrast;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoType = LogoType.Colored;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetTargetSizeIcon(string path, string colorscheme, bool highContrast = false)
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
if (extension != null)
|
||||
{
|
||||
var end = path.Length - extension.Length;
|
||||
var prefix = path.Substring(0, end);
|
||||
var paths = new List<string> { };
|
||||
const int appIconSize = 36;
|
||||
var targetSizes = new List<int> { 16, 24, 30, 36, 44, 60, 72, 96, 128, 180, 256 }.AsParallel();
|
||||
var pathFactorPairs = new Dictionary<string, int>();
|
||||
|
||||
foreach (var factor in targetSizes)
|
||||
{
|
||||
if (highContrast)
|
||||
{
|
||||
var suffixThemePath = $"{prefix}.targetsize-{factor}_{colorscheme}{extension}";
|
||||
var prefixThemePath = $"{prefix}.{colorscheme}_targetsize-{factor}{extension}";
|
||||
paths.Add(suffixThemePath);
|
||||
paths.Add(prefixThemePath);
|
||||
pathFactorPairs.Add(suffixThemePath, factor);
|
||||
pathFactorPairs.Add(prefixThemePath, factor);
|
||||
}
|
||||
else
|
||||
{
|
||||
var simplePath = $"{prefix}.targetsize-{factor}{extension}";
|
||||
var altformUnPlatedPath = $"{prefix}.targetsize-{factor}_altform-unplated{extension}";
|
||||
paths.Add(simplePath);
|
||||
paths.Add(altformUnPlatedPath);
|
||||
pathFactorPairs.Add(simplePath, factor);
|
||||
pathFactorPairs.Add(altformUnPlatedPath, factor);
|
||||
}
|
||||
}
|
||||
|
||||
var selectedIconPath = paths.OrderBy(x => Math.Abs(pathFactorPairs.GetValueOrDefault(x) - appIconSize)).FirstOrDefault(File.Exists);
|
||||
if (!string.IsNullOrEmpty(selectedIconPath))
|
||||
{
|
||||
LogoPath = selectedIconPath;
|
||||
if (highContrast)
|
||||
{
|
||||
LogoType = LogoType.HighContrast;
|
||||
}
|
||||
else
|
||||
{
|
||||
LogoType = LogoType.Colored;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetColoredIcon(string path, string colorscheme)
|
||||
{
|
||||
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
|
||||
if (isSetColoredScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
|
||||
if (isSetColoredTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
|
||||
if (isSetHighContrastScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
|
||||
if (isSetHighContrastTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool SetHighContrastIcon(string path, string colorscheme)
|
||||
{
|
||||
var isSetHighContrastScaleIcon = SetScaleIcons(path, colorscheme, true);
|
||||
if (isSetHighContrastScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetHighContrastTargetIcon = SetTargetSizeIcon(path, colorscheme, true);
|
||||
if (isSetHighContrastTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredScaleIcon = SetScaleIcons(path, colorscheme);
|
||||
if (isSetColoredScaleIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var isSetColoredTargetIcon = SetTargetSizeIcon(path, colorscheme);
|
||||
if (isSetColoredTargetIcon)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal void LogoPathFromUri(string uri, Theme theme)
|
||||
{
|
||||
// all https://learn.microsoft.com/windows/uwp/controls-and-patterns/tiles-and-notifications-app-assets
|
||||
// windows 10 https://msdn.microsoft.com/library/windows/apps/dn934817.aspx
|
||||
// windows 8.1 https://msdn.microsoft.com/library/windows/apps/hh965372.aspx#target_size
|
||||
// windows 8 https://msdn.microsoft.com/library/windows/apps/br211475.aspx
|
||||
string path;
|
||||
bool isLogoUriSet;
|
||||
|
||||
// Using Ordinal since this is used internally with uri
|
||||
if (uri.Contains('\\', StringComparison.Ordinal))
|
||||
{
|
||||
path = Path.Combine(Package.Location, uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// for C:\Windows\MiracastView etc
|
||||
path = Path.Combine(Package.Location, "Assets", uri);
|
||||
}
|
||||
|
||||
switch (theme)
|
||||
{
|
||||
case Theme.HighContrastBlack:
|
||||
case Theme.HighContrastOne:
|
||||
case Theme.HighContrastTwo:
|
||||
isLogoUriSet = SetHighContrastIcon(path, ContrastBlack);
|
||||
break;
|
||||
case Theme.HighContrastWhite:
|
||||
isLogoUriSet = SetHighContrastIcon(path, ContrastWhite);
|
||||
break;
|
||||
case Theme.Light:
|
||||
isLogoUriSet = SetColoredIcon(path, ContrastWhite);
|
||||
break;
|
||||
default:
|
||||
isLogoUriSet = SetColoredIcon(path, ContrastBlack);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isLogoUriSet)
|
||||
{
|
||||
LogoPath = string.Empty;
|
||||
LogoType = LogoType.Error;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
public ImageSource Logo()
|
||||
{
|
||||
if (LogoType == LogoType.Colored)
|
||||
{
|
||||
var logo = ImageFromPath(LogoPath);
|
||||
var platedImage = PlatedImage(logo);
|
||||
return platedImage;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ImageFromPath(LogoPath);
|
||||
}
|
||||
}
|
||||
|
||||
private const int _dpiScale100 = 96;
|
||||
|
||||
private ImageSource PlatedImage(BitmapImage image)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(BackgroundColor))
|
||||
{
|
||||
string currentBackgroundColor;
|
||||
if (BackgroundColor == "transparent")
|
||||
{
|
||||
// Using InvariantCulture since this is internal
|
||||
currentBackgroundColor = SystemParameters.WindowGlassBrush.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentBackgroundColor = BackgroundColor;
|
||||
}
|
||||
|
||||
var padding = 8;
|
||||
var width = image.Width + (2 * padding);
|
||||
var height = image.Height + (2 * padding);
|
||||
var x = 0;
|
||||
var y = 0;
|
||||
|
||||
var group = new DrawingGroup();
|
||||
var converted = ColorConverter.ConvertFromString(currentBackgroundColor);
|
||||
if (converted != null)
|
||||
{
|
||||
var color = (Color)converted;
|
||||
var brush = new SolidColorBrush(color);
|
||||
var pen = new Pen(brush, 1);
|
||||
var backgroundArea = new Rect(0, 0, width, height);
|
||||
var rectangleGeometry = new RectangleGeometry(backgroundArea, 8, 8);
|
||||
var rectDrawing = new GeometryDrawing(brush, pen, rectangleGeometry);
|
||||
group.Children.Add(rectDrawing);
|
||||
|
||||
var imageArea = new Rect(x + padding, y + padding, image.Width, image.Height);
|
||||
var imageDrawing = new ImageDrawing(image, imageArea);
|
||||
group.Children.Add(imageDrawing);
|
||||
|
||||
// http://stackoverflow.com/questions/6676072/get-system-drawing-bitmap-of-a-wpf-area-using-visualbrush
|
||||
var visual = new DrawingVisual();
|
||||
var context = visual.RenderOpen();
|
||||
context.DrawDrawing(group);
|
||||
context.Close();
|
||||
|
||||
var bitmap = new RenderTargetBitmap(
|
||||
Convert.ToInt32(width),
|
||||
Convert.ToInt32(height),
|
||||
_dpiScale100,
|
||||
_dpiScale100,
|
||||
PixelFormats.Pbgra32);
|
||||
|
||||
bitmap.Render(visual);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgramLogger.Exception($"Unable to convert background string {BackgroundColor} to color for {Package.Location}", new InvalidOperationException(), GetType(), Package.Location);
|
||||
|
||||
return new BitmapImage(new Uri(Constant.ErrorIcon));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// todo use windows theme as background
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
private BitmapImage ImageFromPath(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
using (var fileStream = File.OpenRead(path))
|
||||
{
|
||||
fileStream.CopyTo(memoryStream);
|
||||
memoryStream.Position = 0;
|
||||
|
||||
var image = new BitmapImage();
|
||||
image.BeginInit();
|
||||
image.StreamSource = memoryStream;
|
||||
image.EndInit();
|
||||
return image;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ProgramLogger.Exception($"Unable to get logo for {UserModelId} from {path} and located in {Package.Location}", new FileNotFoundException(), GetType(), path);
|
||||
return new BitmapImage(new Uri(ImageLoader.ErrorIconPath));
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{DisplayName}: {Description}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,847 @@
|
||||
// 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.ComponentModel.Design;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.IO.Abstractions;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Security;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.CmdPal.Ext.Apps.Commands;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Serializable]
|
||||
public class Win32Program : IProgram
|
||||
{
|
||||
public static readonly Win32Program InvalidProgram = new Win32Program { Valid = false, Enabled = false };
|
||||
|
||||
private static readonly IFileSystem FileSystem = new FileSystem();
|
||||
private static readonly IPath Path = FileSystem.Path;
|
||||
private static readonly IFile File = FileSystem.File;
|
||||
private static readonly IDirectory Directory = FileSystem.Directory;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
// Localized name based on windows display language
|
||||
public string NameLocalized { get; set; } = string.Empty;
|
||||
|
||||
public string UniqueIdentifier { get; set; } = string.Empty;
|
||||
|
||||
public string IcoPath { get; set; } = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Path of app executable or lnk target executable
|
||||
public string FullPath { get; set; } = string.Empty;
|
||||
|
||||
// Localized path based on windows display language
|
||||
public string FullPathLocalized { get; set; } = string.Empty;
|
||||
|
||||
public string ParentDirectory { get; set; } = string.Empty;
|
||||
|
||||
public string ExecutableName { get; set; } = string.Empty;
|
||||
|
||||
// Localized executable name based on windows display language
|
||||
public string ExecutableNameLocalized { get; set; } = string.Empty;
|
||||
|
||||
// Path to the lnk file on LnkProgram
|
||||
public string LnkFilePath { get; set; } = string.Empty;
|
||||
|
||||
public string LnkResolvedExecutableName { get; set; } = string.Empty;
|
||||
|
||||
// Localized path based on windows display language
|
||||
public string LnkResolvedExecutableNameLocalized { get; set; } = string.Empty;
|
||||
|
||||
public bool Valid { get; set; }
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool HasArguments => !string.IsNullOrEmpty(Arguments);
|
||||
|
||||
public string Arguments { get; set; } = string.Empty;
|
||||
|
||||
public string Location => ParentDirectory;
|
||||
|
||||
public ApplicationType AppType { get; set; }
|
||||
|
||||
// Wrappers for File Operations
|
||||
public static IFileVersionInfoWrapper FileVersionInfoWrapper { get; set; } = new FileVersionInfoWrapper();
|
||||
|
||||
public static IFile FileWrapper { get; set; } = new FileSystem().File;
|
||||
|
||||
private const string ShortcutExtension = "lnk";
|
||||
private const string ApplicationReferenceExtension = "appref-ms";
|
||||
private const string InternetShortcutExtension = "url";
|
||||
private static readonly HashSet<string> ExecutableApplicationExtensions = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "exe", "bat", "bin", "com", "cpl", "msc", "msi", "cmd", "ps1", "job", "msp", "mst", "sct", "ws", "wsh", "wsf" };
|
||||
|
||||
private const string ProxyWebApp = "_proxy.exe";
|
||||
private const string AppIdArgument = "--app-id";
|
||||
|
||||
public enum ApplicationType
|
||||
{
|
||||
WebApplication = 0,
|
||||
InternetShortcutApplication = 1,
|
||||
Win32Application = 2,
|
||||
ShortcutApplication = 3,
|
||||
ApprefApplication = 4,
|
||||
RunCommand = 5,
|
||||
Folder = 6,
|
||||
GenericFile = 7,
|
||||
}
|
||||
|
||||
public bool IsWebApplication()
|
||||
{
|
||||
// To Filter PWAs when the user searches for the main application
|
||||
// All Chromium based applications contain the --app-id argument
|
||||
// Reference : https://codereview.chromium.org/399045
|
||||
// Using Ordinal IgnoreCase since this is used internally
|
||||
return !string.IsNullOrEmpty(FullPath) &&
|
||||
!string.IsNullOrEmpty(Arguments) &&
|
||||
FullPath.Contains(ProxyWebApp, StringComparison.OrdinalIgnoreCase) &&
|
||||
Arguments.Contains(AppIdArgument, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// Condition to Filter pinned Web Applications or PWAs when searching for the main application
|
||||
public bool FilterWebApplication(string query)
|
||||
{
|
||||
// If the app is not a web application, then do not filter it
|
||||
if (!IsWebApplication())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var subqueries = query?.Split() ?? Array.Empty<string>();
|
||||
var nameContainsQuery = false;
|
||||
var pathContainsQuery = false;
|
||||
|
||||
// check if any space separated query is a part of the app name or path name
|
||||
foreach (var subquery in subqueries)
|
||||
{
|
||||
// Using OrdinalIgnoreCase since these are used internally
|
||||
if (FullPath.Contains(subquery, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
pathContainsQuery = true;
|
||||
}
|
||||
|
||||
if (Name.Contains(subquery, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
nameContainsQuery = true;
|
||||
}
|
||||
}
|
||||
|
||||
return pathContainsQuery && !nameContainsQuery;
|
||||
}
|
||||
|
||||
// Function to set the subtitle based on the Type of application
|
||||
public string Type()
|
||||
{
|
||||
switch (AppType)
|
||||
{
|
||||
case ApplicationType.Win32Application:
|
||||
case ApplicationType.ShortcutApplication:
|
||||
case ApplicationType.ApprefApplication:
|
||||
return Resources.application;
|
||||
case ApplicationType.InternetShortcutApplication:
|
||||
return Resources.internet_shortcut_application;
|
||||
case ApplicationType.WebApplication:
|
||||
return Resources.web_application;
|
||||
case ApplicationType.RunCommand:
|
||||
return Resources.run_command;
|
||||
case ApplicationType.Folder:
|
||||
return Resources.folder;
|
||||
case ApplicationType.GenericFile:
|
||||
return Resources.file;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public bool QueryEqualsNameForRunCommands(string query)
|
||||
{
|
||||
if (query != null && AppType == ApplicationType.RunCommand)
|
||||
{
|
||||
// Using OrdinalIgnoreCase since this is used internally
|
||||
if (!query.Equals(Name, StringComparison.OrdinalIgnoreCase) && !query.Equals(ExecutableName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public List<CommandContextItem> GetCommands()
|
||||
{
|
||||
List<CommandContextItem> commands = new List<CommandContextItem>();
|
||||
|
||||
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
|
||||
{
|
||||
commands.Add(new CommandContextItem(
|
||||
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false)));
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory)));
|
||||
}
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new OpenPathCommand(ParentDirectory)));
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new OpenInConsoleCommand(ParentDirectory)));
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return ExecutableName;
|
||||
}
|
||||
|
||||
private static Win32Program CreateWin32Program(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parentDir = Directory.GetParent(path);
|
||||
|
||||
return new Win32Program
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(path),
|
||||
ExecutableName = Path.GetFileName(path),
|
||||
IcoPath = path,
|
||||
|
||||
// Using InvariantCulture since this is user facing
|
||||
FullPath = path,
|
||||
UniqueIdentifier = path,
|
||||
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
|
||||
Description = string.Empty,
|
||||
Valid = true,
|
||||
Enabled = true,
|
||||
AppType = ApplicationType.Win32Application,
|
||||
|
||||
// Localized name, path and executable based on windows display language
|
||||
NameLocalized = ShellLocalization.Instance.GetLocalizedName(path),
|
||||
FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(path),
|
||||
ExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(path)),
|
||||
};
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex InternetShortcutURLPrefixes = new Regex(@"^steam:\/\/(rungameid|run|open)\/|^com\.epicgames\.launcher:\/\/apps\/", RegexOptions.Compiled);
|
||||
|
||||
// This function filters Internet Shortcut programs
|
||||
private static Win32Program InternetShortcutProgram(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
// We don't want to read the whole file if we don't need to
|
||||
var lines = FileWrapper.ReadLines(path);
|
||||
var iconPath = string.Empty;
|
||||
var urlPath = string.Empty;
|
||||
var validApp = false;
|
||||
|
||||
const string urlPrefix = "URL=";
|
||||
const string iconFilePrefix = "IconFile=";
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// Using OrdinalIgnoreCase since this is used internally
|
||||
if (line.StartsWith(urlPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
urlPath = line.Substring(urlPrefix.Length);
|
||||
|
||||
if (!Uri.TryCreate(urlPath, UriKind.RelativeOrAbsolute, out var _))
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
|
||||
// To filter out only those steam shortcuts which have 'run' or 'rungameid' as the hostname
|
||||
if (InternetShortcutURLPrefixes.Match(urlPath).Success)
|
||||
{
|
||||
validApp = true;
|
||||
}
|
||||
}
|
||||
else if (line.StartsWith(iconFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
iconPath = line.Substring(iconFilePrefix.Length);
|
||||
}
|
||||
|
||||
// If we resolved an urlPath & and an iconPath quit reading the file
|
||||
if (!string.IsNullOrEmpty(urlPath) && !string.IsNullOrEmpty(iconPath))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validApp)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parentDir = Directory.GetParent(path);
|
||||
|
||||
return new Win32Program
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(path),
|
||||
ExecutableName = Path.GetFileName(path),
|
||||
IcoPath = iconPath,
|
||||
FullPath = urlPath,
|
||||
UniqueIdentifier = path,
|
||||
ParentDirectory = parentDir is null ? string.Empty : parentDir.FullName,
|
||||
Valid = true,
|
||||
Enabled = true,
|
||||
AppType = ApplicationType.InternetShortcutApplication,
|
||||
};
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
|
||||
private static Win32Program LnkProgram(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var program = CreateWin32Program(path);
|
||||
var shellLinkHelper = new ShellLinkHelper();
|
||||
var target = shellLinkHelper.RetrieveTargetPath(path);
|
||||
|
||||
if (!string.IsNullOrEmpty(target))
|
||||
{
|
||||
if (!(File.Exists(target) || Directory.Exists(target)))
|
||||
{
|
||||
// If the link points nowhere, consider it invalid.
|
||||
return InvalidProgram;
|
||||
}
|
||||
|
||||
program.LnkFilePath = program.FullPath;
|
||||
program.LnkResolvedExecutableName = Path.GetFileName(target);
|
||||
program.LnkResolvedExecutableNameLocalized = Path.GetFileName(ShellLocalization.Instance.GetLocalizedPath(target));
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
program.FullPath = Path.GetFullPath(target);
|
||||
program.FullPathLocalized = ShellLocalization.Instance.GetLocalizedPath(target);
|
||||
|
||||
program.Arguments = shellLinkHelper.Arguments;
|
||||
|
||||
// A .lnk could be a (Chrome) PWA, set correct AppType
|
||||
program.AppType = program.IsWebApplication()
|
||||
? ApplicationType.WebApplication
|
||||
: GetAppTypeFromPath(target);
|
||||
|
||||
var description = shellLinkHelper.Description;
|
||||
if (!string.IsNullOrEmpty(description))
|
||||
{
|
||||
program.Description = description;
|
||||
}
|
||||
else
|
||||
{
|
||||
var info = FileVersionInfoWrapper.GetVersionInfo(target);
|
||||
if (!string.IsNullOrEmpty(info?.FileDescription))
|
||||
{
|
||||
program.Description = info.FileDescription;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
catch (System.IO.FileLoadException)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
|
||||
// Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
|
||||
// Error caused likely due to trying to get the description of the program
|
||||
catch (Exception)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
|
||||
private static Win32Program ExeProgram(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var program = CreateWin32Program(path);
|
||||
var info = FileVersionInfoWrapper.GetVersionInfo(path);
|
||||
if (!string.IsNullOrEmpty(info?.FileDescription))
|
||||
{
|
||||
program.Description = info.FileDescription;
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to get the application type, given the path to the application
|
||||
public static ApplicationType GetAppTypeFromPath(string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
var extension = Extension(path);
|
||||
|
||||
// Using OrdinalIgnoreCase since these are used internally with paths
|
||||
if (ExecutableApplicationExtensions.Contains(extension))
|
||||
{
|
||||
return ApplicationType.Win32Application;
|
||||
}
|
||||
else if (extension.Equals(ShortcutExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ApplicationType.ShortcutApplication;
|
||||
}
|
||||
else if (extension.Equals(ApplicationReferenceExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ApplicationType.ApprefApplication;
|
||||
}
|
||||
else if (extension.Equals(InternetShortcutExtension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ApplicationType.InternetShortcutApplication;
|
||||
}
|
||||
else if (string.IsNullOrEmpty(extension) && System.IO.Directory.Exists(path))
|
||||
{
|
||||
return ApplicationType.Folder;
|
||||
}
|
||||
|
||||
return ApplicationType.GenericFile;
|
||||
}
|
||||
|
||||
// Function to get the Win32 application, given the path to the application
|
||||
public static Win32Program? GetAppFromPath(string path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
Win32Program? app;
|
||||
switch (GetAppTypeFromPath(path))
|
||||
{
|
||||
case ApplicationType.Win32Application:
|
||||
app = ExeProgram(path);
|
||||
break;
|
||||
case ApplicationType.ShortcutApplication:
|
||||
app = LnkProgram(path);
|
||||
break;
|
||||
case ApplicationType.ApprefApplication:
|
||||
app = CreateWin32Program(path);
|
||||
app.AppType = ApplicationType.ApprefApplication;
|
||||
break;
|
||||
case ApplicationType.InternetShortcutApplication:
|
||||
app = InternetShortcutProgram(path);
|
||||
break;
|
||||
case ApplicationType.WebApplication:
|
||||
case ApplicationType.RunCommand:
|
||||
case ApplicationType.Folder:
|
||||
case ApplicationType.GenericFile:
|
||||
default:
|
||||
app = null;
|
||||
break;
|
||||
}
|
||||
|
||||
// if the app is valid, only then return the application, else return null
|
||||
return app?.Valid == true
|
||||
? app
|
||||
: null;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ProgramPaths(string directory, IList<string> suffixes, bool recursiveSearch = true)
|
||||
{
|
||||
if (!Directory.Exists(directory))
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
var files = new List<string>();
|
||||
var folderQueue = new Queue<string>();
|
||||
folderQueue.Enqueue(directory);
|
||||
|
||||
// Keep track of already visited directories to avoid cycles.
|
||||
var alreadyVisited = new HashSet<string>();
|
||||
|
||||
do
|
||||
{
|
||||
var currentDirectory = folderQueue.Dequeue();
|
||||
|
||||
if (alreadyVisited.Contains(currentDirectory))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
alreadyVisited.Add(currentDirectory);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var suffix in suffixes)
|
||||
{
|
||||
try
|
||||
{
|
||||
files.AddRange(Directory.EnumerateFiles(currentDirectory, $"*.{suffix}", SearchOption.TopDirectoryOnly));
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If the search is set to be non-recursive, then do not enqueue the child directories.
|
||||
if (!recursiveSearch)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var childDirectory in Directory.EnumerateDirectories(currentDirectory, "*", new EnumerationOptions()
|
||||
{
|
||||
// https://learn.microsoft.com/dotnet/api/system.io.enumerationoptions?view=net-6.0
|
||||
// Exclude directories with the Reparse Point file attribute, to avoid loops due to symbolic links / directory junction / mount points.
|
||||
AttributesToSkip = FileAttributes.Hidden | FileAttributes.System | FileAttributes.ReparsePoint,
|
||||
RecurseSubdirectories = false,
|
||||
}))
|
||||
{
|
||||
folderQueue.Enqueue(childDirectory);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
}
|
||||
while (folderQueue.Count > 0);
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
private static string Extension(string path)
|
||||
{
|
||||
// Using InvariantCulture since this is user facing
|
||||
var extension = Path.GetExtension(path)?.ToLowerInvariant();
|
||||
|
||||
return !string.IsNullOrEmpty(extension)
|
||||
? extension.Substring(1)
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> CustomProgramPaths(IEnumerable<ProgramSource> sources, IList<string> suffixes)
|
||||
=> sources?.Where(programSource => Directory.Exists(programSource.Location) && programSource.Enabled)
|
||||
.SelectMany(programSource => ProgramPaths(programSource.Location, suffixes))
|
||||
.ToList() ?? Enumerable.Empty<string>();
|
||||
|
||||
// Function to obtain the list of applications, the locations of which have been added to the env variable PATH
|
||||
private static List<string> PathEnvironmentProgramPaths(IList<string> suffixes)
|
||||
{
|
||||
// To get all the locations stored in the PATH env variable
|
||||
var pathEnvVariable = Environment.GetEnvironmentVariable("PATH");
|
||||
var searchPaths = pathEnvVariable?.Split(Path.PathSeparator);
|
||||
var toFilterAllPaths = new List<string>();
|
||||
var isRecursiveSearch = true;
|
||||
|
||||
if (searchPaths is not null)
|
||||
{
|
||||
foreach (var path in searchPaths)
|
||||
{
|
||||
if (path.Length > 0)
|
||||
{
|
||||
// to expand any environment variables present in the path
|
||||
var directory = Environment.ExpandEnvironmentVariables(path);
|
||||
var paths = ProgramPaths(directory, suffixes, !isRecursiveSearch);
|
||||
toFilterAllPaths.AddRange(paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return toFilterAllPaths;
|
||||
}
|
||||
|
||||
private static List<string> IndexPath(IList<string> suffixes, List<string> indexLocations)
|
||||
=> indexLocations
|
||||
.SelectMany(indexLocation => ProgramPaths(indexLocation, suffixes))
|
||||
.ToList();
|
||||
|
||||
private static List<string> StartMenuProgramPaths(IList<string> suffixes)
|
||||
{
|
||||
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
|
||||
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
|
||||
var indexLocation = new List<string>() { directory1, directory2 };
|
||||
|
||||
return IndexPath(suffixes, indexLocation);
|
||||
}
|
||||
|
||||
private static List<string> DesktopProgramPaths(IList<string> suffixes)
|
||||
{
|
||||
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
|
||||
|
||||
var indexLocation = new List<string>() { directory1, directory2 };
|
||||
|
||||
return IndexPath(suffixes, indexLocation);
|
||||
}
|
||||
|
||||
private static List<string> RegistryAppProgramPaths(IList<string> suffixes)
|
||||
{
|
||||
// https://msdn.microsoft.com/library/windows/desktop/ee872121
|
||||
const string appPaths = @"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths";
|
||||
var paths = new List<string>();
|
||||
using (var root = Registry.LocalMachine.OpenSubKey(appPaths))
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
paths.AddRange(GetPathsFromRegistry(root));
|
||||
}
|
||||
}
|
||||
|
||||
using (var root = Registry.CurrentUser.OpenSubKey(appPaths))
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
paths.AddRange(GetPathsFromRegistry(root));
|
||||
}
|
||||
}
|
||||
|
||||
return paths
|
||||
.Where(path => suffixes.Any(suffix => path.EndsWith(suffix, StringComparison.InvariantCultureIgnoreCase)))
|
||||
.Select(ExpandEnvironmentVariables)
|
||||
.Where(path => path is not null)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetPathsFromRegistry(RegistryKey root)
|
||||
=> root
|
||||
.GetSubKeyNames()
|
||||
.Select(x => GetPathFromRegistrySubkey(root, x));
|
||||
|
||||
private static string GetPathFromRegistrySubkey(RegistryKey root, string subkey)
|
||||
{
|
||||
var path = string.Empty;
|
||||
try
|
||||
{
|
||||
using (var key = root.OpenSubKey(subkey))
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var defaultValue = string.Empty;
|
||||
path = key.GetValue(defaultValue) as string;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// fix path like this: ""\"C:\\folder\\executable.exe\""
|
||||
return path = path.Trim('"', ' ');
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ExpandEnvironmentVariables(string path) =>
|
||||
!string.IsNullOrEmpty(path)
|
||||
? Environment.ExpandEnvironmentVariables(path)
|
||||
: string.Empty;
|
||||
|
||||
// Overriding the object.GetHashCode() function to aid in removing duplicates while adding and removing apps from the concurrent dictionary storage
|
||||
public override int GetHashCode()
|
||||
=> Win32ProgramEqualityComparer.Default.GetHashCode(this);
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is Win32Program win32Program && Win32ProgramEqualityComparer.Default.Equals(this, win32Program);
|
||||
|
||||
private sealed class Win32ProgramEqualityComparer : IEqualityComparer<Win32Program>
|
||||
{
|
||||
public static readonly Win32ProgramEqualityComparer Default = new Win32ProgramEqualityComparer();
|
||||
|
||||
public bool Equals(Win32Program? app1, Win32Program? app2)
|
||||
{
|
||||
if (app1 == null && app2 == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return app1 != null
|
||||
&& app2 != null
|
||||
&& (app1.Name?.ToUpperInvariant(), app1.ExecutableName?.ToUpperInvariant(), app1.FullPath?.ToUpperInvariant())
|
||||
.Equals((app2.Name?.ToUpperInvariant(), app2.ExecutableName?.ToUpperInvariant(), app2.FullPath?.ToUpperInvariant()));
|
||||
}
|
||||
|
||||
public int GetHashCode(Win32Program obj)
|
||||
=> (obj.Name?.ToUpperInvariant(), obj.ExecutableName?.ToUpperInvariant(), obj.FullPath?.ToUpperInvariant()).GetHashCode();
|
||||
}
|
||||
|
||||
public static List<Win32Program> DeduplicatePrograms(IEnumerable<Win32Program> programs)
|
||||
=> new HashSet<Win32Program>(programs, Win32ProgramEqualityComparer.Default).ToList();
|
||||
|
||||
private static Win32Program GetProgramFromPath(string path)
|
||||
{
|
||||
var extension = Extension(path);
|
||||
if (ExecutableApplicationExtensions.Contains(extension))
|
||||
{
|
||||
return ExeProgram(path);
|
||||
}
|
||||
|
||||
switch (extension)
|
||||
{
|
||||
case ShortcutExtension:
|
||||
return LnkProgram(path);
|
||||
case ApplicationReferenceExtension:
|
||||
return CreateWin32Program(path);
|
||||
case InternetShortcutExtension:
|
||||
return InternetShortcutProgram(path);
|
||||
default:
|
||||
return InvalidProgram;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetIcoPathForRunCommandProgram(Win32Program program, out string? icoPath)
|
||||
{
|
||||
icoPath = null;
|
||||
|
||||
if (program.AppType != ApplicationType.RunCommand)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(program.FullPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://msdn.microsoft.com/library/windows/desktop/ee872121
|
||||
try
|
||||
{
|
||||
var redirectionPath = ReparsePoint.GetTarget(program.FullPath);
|
||||
if (string.IsNullOrEmpty(redirectionPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
icoPath = ExpandEnvironmentVariables(redirectionPath);
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
|
||||
icoPath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Win32Program GetRunCommandProgramFromPath(string path)
|
||||
{
|
||||
var program = GetProgramFromPath(path);
|
||||
if (program.Valid)
|
||||
{
|
||||
program.AppType = ApplicationType.RunCommand;
|
||||
|
||||
if (TryGetIcoPathForRunCommandProgram(program, out var icoPath))
|
||||
{
|
||||
program.IcoPath = icoPath ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
return program;
|
||||
}
|
||||
|
||||
public static IList<Win32Program> All(AllAppsSettings settings)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settings);
|
||||
|
||||
try
|
||||
{
|
||||
// Set an initial size to an expected size to prevent multiple hashSet resizes
|
||||
const int defaultHashsetSize = 1000;
|
||||
|
||||
// Multiple paths could have the same programPaths and we don't want to resolve / lookup them multiple times
|
||||
var paths = new HashSet<string>(defaultHashsetSize);
|
||||
var runCommandPaths = new HashSet<string>(defaultHashsetSize);
|
||||
|
||||
// Parallelize multiple sources, and priority based on paths which most likely contain .lnks which are formatted
|
||||
var sources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
|
||||
{
|
||||
(true, () => CustomProgramPaths(settings.ProgramSources, settings.ProgramSuffixes)),
|
||||
(settings.EnableStartMenuSource, () => StartMenuProgramPaths(settings.ProgramSuffixes)),
|
||||
(settings.EnableDesktopSource, () => DesktopProgramPaths(settings.ProgramSuffixes)),
|
||||
(settings.EnableRegistrySource, () => RegistryAppProgramPaths(settings.ProgramSuffixes)),
|
||||
};
|
||||
|
||||
// Run commands are always set as AppType "RunCommand"
|
||||
var runCommandSources = new (bool IsEnabled, Func<IEnumerable<string>> GetPaths)[]
|
||||
{
|
||||
(settings.EnablePathEnvironmentVariableSource, () => PathEnvironmentProgramPaths(settings.RunCommandSuffixes)),
|
||||
};
|
||||
|
||||
var disabledProgramsList = settings.DisabledProgramSources;
|
||||
|
||||
// Get all paths but exclude all normal .Executables
|
||||
paths.UnionWith(sources
|
||||
.AsParallel()
|
||||
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
|
||||
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath))
|
||||
.Where(path => !ExecutableApplicationExtensions.Contains(Extension(path))));
|
||||
runCommandPaths.UnionWith(runCommandSources
|
||||
.AsParallel()
|
||||
.SelectMany(source => source.IsEnabled ? source.GetPaths() : Enumerable.Empty<string>())
|
||||
.Where(programPath => disabledProgramsList.All(x => x.UniqueIdentifier != programPath)));
|
||||
|
||||
var programs = paths.AsParallel().Select(source => GetProgramFromPath(source));
|
||||
var runCommandPrograms = runCommandPaths.AsParallel().Select(source => GetRunCommandProgramFromPath(source));
|
||||
|
||||
return DeduplicatePrograms(programs.Concat(runCommandPrograms).Where(program => program?.Valid == true));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return Array.Empty<Win32Program>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// 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.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal sealed partial class AddBookmarkForm : FormContent
|
||||
{
|
||||
internal event TypedEventHandler<object, BookmarkData>? AddedCommand;
|
||||
|
||||
private readonly BookmarkData? _bookmark;
|
||||
|
||||
public AddBookmarkForm(BookmarkData? bookmark)
|
||||
{
|
||||
_bookmark = bookmark;
|
||||
var name = _bookmark?.Name ?? string.Empty;
|
||||
var url = _bookmark?.Bookmark ?? string.Empty;
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"style": "text",
|
||||
"id": "name",
|
||||
"label": "{{Resources.bookmarks_form_name_label}}",
|
||||
"value": {{JsonSerializer.Serialize(name)}},
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
|
||||
},
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"style": "text",
|
||||
"id": "bookmark",
|
||||
"value": {{JsonSerializer.Serialize(url)}},
|
||||
"label": "{{Resources.bookmarks_form_bookmark_label}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Resources.bookmarks_form_save}}",
|
||||
"data": {
|
||||
"name": "name",
|
||||
"bookmark": "bookmark"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var formInput = JsonNode.Parse(payload);
|
||||
if (formInput == null)
|
||||
{
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
// get the name and url out of the values
|
||||
var formName = formInput["name"] ?? string.Empty;
|
||||
var formBookmark = formInput["bookmark"] ?? string.Empty;
|
||||
var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}');
|
||||
|
||||
// Determine the type of the bookmark
|
||||
string bookmarkType;
|
||||
|
||||
if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
bookmarkType = "web";
|
||||
}
|
||||
else if (File.Exists(formBookmark.ToString()))
|
||||
{
|
||||
bookmarkType = "file";
|
||||
}
|
||||
else if (Directory.Exists(formBookmark.ToString()))
|
||||
{
|
||||
bookmarkType = "folder";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to web if we can't determine the type
|
||||
bookmarkType = "web";
|
||||
}
|
||||
|
||||
var updated = _bookmark ?? new BookmarkData();
|
||||
updated.Name = formName.ToString();
|
||||
updated.Bookmark = formBookmark.ToString();
|
||||
updated.Type = bookmarkType;
|
||||
|
||||
AddedCommand?.Invoke(this, updated);
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal sealed partial class AddBookmarkPage : ContentPage
|
||||
{
|
||||
private readonly AddBookmarkForm _addBookmark;
|
||||
|
||||
internal event TypedEventHandler<object, BookmarkData>? AddedCommand
|
||||
{
|
||||
add => _addBookmark.AddedCommand += value;
|
||||
remove => _addBookmark.AddedCommand -= value;
|
||||
}
|
||||
|
||||
public override IContent[] GetContent() => [_addBookmark];
|
||||
|
||||
public AddBookmarkPage(BookmarkData? bookmark)
|
||||
{
|
||||
var name = bookmark?.Name ?? string.Empty;
|
||||
var url = bookmark?.Bookmark ?? string.Empty;
|
||||
Icon = new IconInfo("\ued0e");
|
||||
var isAdd = string.IsNullOrEmpty(name) && string.IsNullOrEmpty(url);
|
||||
Title = isAdd ? Resources.bookmarks_add_title : Resources.bookmarks_edit_name;
|
||||
Name = isAdd ? Resources.bookmarks_add_name : Resources.bookmarks_edit_name;
|
||||
_addBookmark = new(bookmark);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Text.Json.Serialization;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
public class BookmarkData
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Bookmark { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}');
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal sealed partial class BookmarkPlaceholderForm : FormContent
|
||||
{
|
||||
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Resources.bookmarks_required_placeholder);
|
||||
|
||||
private readonly List<string> _placeholderNames;
|
||||
|
||||
private readonly string _bookmark = string.Empty;
|
||||
|
||||
// TODO pass in an array of placeholders
|
||||
public BookmarkPlaceholderForm(string name, string url, string type)
|
||||
{
|
||||
_bookmark = url;
|
||||
var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}"));
|
||||
var matches = r.Matches(url);
|
||||
_placeholderNames = matches.Select(m => m.Groups[1].Value).ToList();
|
||||
var inputs = _placeholderNames.Select(p =>
|
||||
{
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, ErrorMessage, p);
|
||||
return $$"""
|
||||
{
|
||||
"type": "Input.Text",
|
||||
"style": "text",
|
||||
"id": "{{p}}",
|
||||
"label": "{{p}}",
|
||||
"isRequired": true,
|
||||
"errorMessage": "{{errorMessage}}"
|
||||
}
|
||||
""";
|
||||
}).ToList();
|
||||
|
||||
var allInputs = string.Join(",", inputs);
|
||||
|
||||
TemplateJson = $$"""
|
||||
{
|
||||
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
|
||||
"type": "AdaptiveCard",
|
||||
"version": "1.5",
|
||||
"body": [
|
||||
""" + allInputs + $$"""
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": "Action.Submit",
|
||||
"title": "{{Resources.bookmarks_form_open}}",
|
||||
"data": {
|
||||
"placeholder": "placeholder"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
}
|
||||
|
||||
public override CommandResult SubmitForm(string payload)
|
||||
{
|
||||
var target = _bookmark;
|
||||
|
||||
// parse the submitted JSON and then open the link
|
||||
var formInput = JsonNode.Parse(payload);
|
||||
var formObject = formInput?.AsObject();
|
||||
if (formObject == null)
|
||||
{
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
|
||||
foreach (var (key, value) in formObject)
|
||||
{
|
||||
var placeholderString = $"{{{key}}}";
|
||||
var placeholderData = value?.ToString();
|
||||
target = target.Replace(placeholderString, placeholderData);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var uri = UrlCommand.GetUri(target);
|
||||
if (uri != null)
|
||||
{
|
||||
_ = Launcher.LaunchUriAsync(uri);
|
||||
}
|
||||
else
|
||||
{
|
||||
// throw new UriFormatException("The provided URL is not valid.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"Error launching URL: {ex.Message}");
|
||||
}
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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 Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
internal sealed partial class BookmarkPlaceholderPage : ContentPage
|
||||
{
|
||||
private readonly FormContent _bookmarkPlaceholder;
|
||||
|
||||
public override IContent[] GetContent() => [_bookmarkPlaceholder];
|
||||
|
||||
public BookmarkPlaceholderPage(BookmarkData data)
|
||||
: this(data.Name, data.Bookmark, data.Type)
|
||||
{
|
||||
}
|
||||
|
||||
public BookmarkPlaceholderPage(string name, string url, string type)
|
||||
{
|
||||
Name = name;
|
||||
Icon = new IconInfo(UrlCommand.IconFromUrl(url, type));
|
||||
_bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
public sealed class Bookmarks
|
||||
{
|
||||
public List<BookmarkData> Data { get; set; } = [];
|
||||
|
||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
IncludeFields = true,
|
||||
};
|
||||
|
||||
public static Bookmarks ReadFromFile(string path)
|
||||
{
|
||||
var data = new Bookmarks();
|
||||
|
||||
// if the file exists, load it and append the new item
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var jsonStringReading = File.ReadAllText(path);
|
||||
|
||||
if (!string.IsNullOrEmpty(jsonStringReading))
|
||||
{
|
||||
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, _jsonOptions) ?? new Bookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void WriteToFile(string path, Bookmarks data)
|
||||
{
|
||||
var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
|
||||
|
||||
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.Bookmarks.Properties;
|
||||
using Microsoft.CmdPal.Ext.Indexer;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
|
||||
public partial class BookmarksCommandProvider : CommandProvider
|
||||
{
|
||||
private readonly List<CommandItem> _commands = [];
|
||||
|
||||
private readonly AddBookmarkPage _addNewCommand = new(null);
|
||||
|
||||
private Bookmarks? _bookmarks;
|
||||
|
||||
public static IconInfo DeleteIcon { get; private set; } = new("\uE74D"); // Delete
|
||||
|
||||
public static IconInfo EditIcon { get; private set; } = new("\uE70F"); // Edit
|
||||
|
||||
public BookmarksCommandProvider()
|
||||
{
|
||||
Id = "Bookmarks";
|
||||
DisplayName = Resources.bookmarks_display_name;
|
||||
Icon = new IconInfo("\uE718"); // Pin
|
||||
|
||||
_addNewCommand.AddedCommand += AddNewCommand_AddedCommand;
|
||||
}
|
||||
|
||||
private void AddNewCommand_AddedCommand(object sender, BookmarkData args)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})");
|
||||
if (_bookmarks != null)
|
||||
{
|
||||
_bookmarks.Data.Add(args);
|
||||
}
|
||||
|
||||
SaveAndUpdateCommands();
|
||||
}
|
||||
|
||||
// In the edit path, `args` was already in _bookmarks, we just updated it
|
||||
private void Edit_AddedCommand(object sender, BookmarkData args)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Edited bookmark ({args.Name},{args.Bookmark})");
|
||||
|
||||
SaveAndUpdateCommands();
|
||||
}
|
||||
|
||||
private void SaveAndUpdateCommands()
|
||||
{
|
||||
if (_bookmarks != null)
|
||||
{
|
||||
var jsonPath = BookmarksCommandProvider.StateJsonPath();
|
||||
Bookmarks.WriteToFile(jsonPath, _bookmarks);
|
||||
}
|
||||
|
||||
LoadCommands();
|
||||
RaiseItemsChanged(0);
|
||||
}
|
||||
|
||||
private void LoadCommands()
|
||||
{
|
||||
List<CommandItem> collected = [];
|
||||
collected.Add(new CommandItem(_addNewCommand));
|
||||
|
||||
if (_bookmarks == null)
|
||||
{
|
||||
LoadBookmarksFromFile();
|
||||
}
|
||||
|
||||
if (_bookmarks != null)
|
||||
{
|
||||
collected.AddRange(_bookmarks.Data.Select(BookmarkToCommandItem));
|
||||
}
|
||||
|
||||
_commands.Clear();
|
||||
_commands.AddRange(collected);
|
||||
}
|
||||
|
||||
private void LoadBookmarksFromFile()
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonFile = StateJsonPath();
|
||||
if (File.Exists(jsonFile))
|
||||
{
|
||||
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// debug log error
|
||||
Debug.WriteLine($"Error loading commands: {ex.Message}");
|
||||
}
|
||||
|
||||
if (_bookmarks == null)
|
||||
{
|
||||
_bookmarks = new();
|
||||
}
|
||||
}
|
||||
|
||||
private CommandItem BookmarkToCommandItem(BookmarkData bookmark)
|
||||
{
|
||||
ICommand command = bookmark.IsPlaceholder ?
|
||||
new BookmarkPlaceholderPage(bookmark) :
|
||||
new UrlCommand(bookmark);
|
||||
|
||||
var listItem = new CommandItem(command) { Icon = command.Icon };
|
||||
|
||||
List<CommandContextItem> contextMenu = [];
|
||||
|
||||
// Add commands for folder types
|
||||
if (command is UrlCommand urlCommand)
|
||||
{
|
||||
if (urlCommand.Type == "folder")
|
||||
{
|
||||
contextMenu.Add(
|
||||
new CommandContextItem(new DirectoryPage(urlCommand.Url)));
|
||||
|
||||
contextMenu.Add(
|
||||
new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url)));
|
||||
}
|
||||
|
||||
listItem.Subtitle = urlCommand.Url;
|
||||
}
|
||||
|
||||
var edit = new AddBookmarkPage(bookmark) { Icon = EditIcon };
|
||||
edit.AddedCommand += Edit_AddedCommand;
|
||||
contextMenu.Add(new CommandContextItem(edit));
|
||||
|
||||
var delete = new CommandContextItem(
|
||||
title: Resources.bookmarks_delete_title,
|
||||
name: Resources.bookmarks_delete_name,
|
||||
action: () =>
|
||||
{
|
||||
if (_bookmarks != null)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Deleting bookmark ({bookmark.Name},{bookmark.Bookmark})");
|
||||
|
||||
_bookmarks.Data.Remove(bookmark);
|
||||
|
||||
SaveAndUpdateCommands();
|
||||
}
|
||||
},
|
||||
result: CommandResult.KeepOpen())
|
||||
{
|
||||
IsCritical = true,
|
||||
Icon = DeleteIcon,
|
||||
};
|
||||
contextMenu.Add(delete);
|
||||
|
||||
listItem.MoreCommands = contextMenu.ToArray();
|
||||
|
||||
return listItem;
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
if (_commands.Count == 0)
|
||||
{
|
||||
LoadCommands();
|
||||
}
|
||||
|
||||
return _commands.ToArray();
|
||||
}
|
||||
|
||||
internal static string StateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return System.IO.Path.Combine(directory, "bookmarks.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Bookmarks.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
<ProjectReference Include="..\..\Exts\Microsoft.CmdPal.Ext.Indexer\Microsoft.CmdPal.Ext.Indexer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
After Width: | Height: | Size: 3.6 KiB |
@@ -0,0 +1,45 @@
|
||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13.3333 17H2.66667C2.29848 17 2 16.7015 2 16.3333V1.66667C2 1.29848 2.29848 1 2.66667 1H13.3333C13.7015 1 14 1.29848 14 1.66667V16.3333C14 16.7015 13.7015 17 13.3333 17Z" fill="url(#paint0_linear_1825_17892)"/>
|
||||
<g filter="url(#filter0_dd_1825_17892)">
|
||||
<path d="M12.333 2.33398H3.66634C3.48224 2.33398 3.33301 2.48322 3.33301 2.66732V4.66732C3.33301 4.85141 3.48224 5.00065 3.66634 5.00065H12.333C12.5171 5.00065 12.6663 4.85141 12.6663 4.66732V2.66732C12.6663 2.48322 12.5171 2.33398 12.333 2.33398Z" fill="url(#paint1_linear_1825_17892)"/>
|
||||
</g>
|
||||
<path d="M5.66634 9.00065H3.66634C3.48224 9.00065 3.33301 8.85141 3.33301 8.66732V6.66732C3.33301 6.48322 3.48224 6.33398 3.66634 6.33398H5.66634C5.85044 6.33398 5.99967 6.48322 5.99967 6.66732V8.66732C5.99967 8.85141 5.85044 9.00065 5.66634 9.00065Z" fill="#CAD2D9"/>
|
||||
<path d="M9.00033 9.00065H7.00033C6.81623 9.00065 6.66699 8.85141 6.66699 8.66732V6.66732C6.66699 6.48322 6.81623 6.33398 7.00033 6.33398H9.00033C9.18442 6.33398 9.33366 6.48322 9.33366 6.66732V8.66732C9.33366 8.85141 9.18443 9.00065 9.00033 9.00065Z" fill="#CAD2D9"/>
|
||||
<path d="M12.3333 9.00065H10.3333C10.1492 9.00065 10 8.85141 10 8.66732V6.66732C10 6.48322 10.1492 6.33398 10.3333 6.33398H12.3333C12.5174 6.33398 12.6667 6.48322 12.6667 6.66732V8.66732C12.6667 8.85141 12.5174 9.00065 12.3333 9.00065Z" fill="#CAD2D9"/>
|
||||
<path d="M5.66634 12.3327H3.66634C3.48224 12.3327 3.33301 12.1834 3.33301 11.9993V9.99935C3.33301 9.81526 3.48224 9.66602 3.66634 9.66602H5.66634C5.85044 9.66602 5.99967 9.81526 5.99967 9.99935V11.9993C5.99967 12.1834 5.85044 12.3327 5.66634 12.3327Z" fill="#CAD2D9"/>
|
||||
<path d="M9.00033 12.3327H7.00033C6.81623 12.3327 6.66699 12.1834 6.66699 11.9993V9.99935C6.66699 9.81526 6.81623 9.66602 7.00033 9.66602H9.00033C9.18442 9.66602 9.33366 9.81526 9.33366 9.99935V11.9993C9.33366 12.1834 9.18443 12.3327 9.00033 12.3327Z" fill="#CAD2D9"/>
|
||||
<path d="M12.3333 15.666H10.3333C10.1492 15.666 10 15.5168 10 15.3327V9.99935C10 9.81526 10.1492 9.66602 10.3333 9.66602H12.3333C12.5174 9.66602 12.6667 9.81526 12.6667 9.99935V15.3327C12.6667 15.5168 12.5174 15.666 12.3333 15.666Z" fill="url(#paint2_linear_1825_17892)"/>
|
||||
<path d="M5.66634 15.6667H3.66634C3.48224 15.6667 3.33301 15.5174 3.33301 15.3333V13.3333C3.33301 13.1492 3.48224 13 3.66634 13H5.66634C5.85044 13 5.99967 13.1492 5.99967 13.3333V15.3333C5.99967 15.5174 5.85044 15.6667 5.66634 15.6667Z" fill="#CAD2D9"/>
|
||||
<path d="M9.00033 15.6667H7.00033C6.81623 15.6667 6.66699 15.5174 6.66699 15.3333V13.3333C6.66699 13.1492 6.81623 13 7.00033 13H9.00033C9.18442 13 9.33366 13.1492 9.33366 13.3333V15.3333C9.33366 15.5174 9.18443 15.6667 9.00033 15.6667Z" fill="#CAD2D9"/>
|
||||
<defs>
|
||||
<filter id="filter0_dd_1825_17892" x="0.333008" y="0.333984" width="15.333" height="8.66602" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="0.5"/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_17892"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1825_17892" result="effect2_dropShadow_1825_17892"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_17892" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1825_17892" x1="12.8421" y1="17.3868" x2="3.15785" y2="0.613153" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#626F7A"/>
|
||||
<stop offset="1" stop-color="#8B9299"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1825_17892" x1="9.68271" y1="6.58243" x2="6.31664" y2="0.752205" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#28AFEA"/>
|
||||
<stop offset="0.37387" stop-color="#3CCAF4"/>
|
||||
<stop offset="0.74949" stop-color="#4BDFFC"/>
|
||||
<stop offset="1" stop-color="#50E6FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1825_17892" x1="12.9047" y1="15.3878" x2="9.76194" y2="9.94428" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#173A73"/>
|
||||
<stop offset="0.55519" stop-color="#134584"/>
|
||||
<stop offset="1" stop-color="#114A8B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,176 @@
|
||||
// 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.Data;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using Microsoft.CmdPal.Ext.Calc.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Calc;
|
||||
|
||||
public partial class CalculatorCommandProvider : CommandProvider
|
||||
{
|
||||
private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle };
|
||||
private readonly FallbackCalculatorItem _fallback = new();
|
||||
|
||||
public CalculatorCommandProvider()
|
||||
{
|
||||
Id = "Calculator";
|
||||
DisplayName = Resources.calculator_display_name;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem];
|
||||
|
||||
public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
|
||||
}
|
||||
|
||||
// The calculator page is a dynamic list page
|
||||
// * The first command is where we display the results. Title=result, Subtitle=query
|
||||
// - The default command is `SaveCommand`.
|
||||
// - When you save, insert into list at spot 1
|
||||
// - change SearchText to the result
|
||||
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
|
||||
// * The rest of the items are previously saved results
|
||||
// - Command is a CopyCommand
|
||||
// - Each item also sets the TextToSuggest to the result
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
public sealed partial class CalculatorListPage : DynamicListPage
|
||||
{
|
||||
private readonly List<ListItem> _items = [];
|
||||
private readonly SaveCommand _saveCommand = new();
|
||||
private readonly CopyTextCommand _copyContextCommand;
|
||||
private readonly CommandContextItem _copyContextMenuItem;
|
||||
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error);
|
||||
|
||||
public CalculatorListPage()
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
|
||||
Name = Resources.calculator_title;
|
||||
PlaceholderText = Resources.calculator_placeholder_text;
|
||||
Id = "com.microsoft.cmdpal.calculator";
|
||||
|
||||
_copyContextCommand = new CopyTextCommand(string.Empty);
|
||||
_copyContextMenuItem = new CommandContextItem(_copyContextCommand);
|
||||
|
||||
_items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") });
|
||||
|
||||
UpdateSearchText(string.Empty, string.Empty);
|
||||
|
||||
_saveCommand.SaveRequested += HandleSave;
|
||||
}
|
||||
|
||||
private void HandleSave(object sender, object args)
|
||||
{
|
||||
var lastResult = _items[0].Title;
|
||||
if (!string.IsNullOrEmpty(lastResult))
|
||||
{
|
||||
var li = new ListItem(new CopyTextCommand(lastResult))
|
||||
{
|
||||
Title = _items[0].Title,
|
||||
Subtitle = _items[0].Subtitle,
|
||||
TextToSuggest = lastResult,
|
||||
};
|
||||
_items.Insert(1, li);
|
||||
_items[0].Subtitle = string.Empty;
|
||||
SearchText = lastResult;
|
||||
this.RaiseItemsChanged(this._items.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
var firstItem = _items[0];
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
firstItem.Title = Resources.calculator_placeholder_text;
|
||||
firstItem.Subtitle = string.Empty;
|
||||
firstItem.MoreCommands = [];
|
||||
}
|
||||
else
|
||||
{
|
||||
_copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty;
|
||||
firstItem.Title = result;
|
||||
firstItem.Subtitle = newSearch;
|
||||
firstItem.MoreCommands = [_copyContextMenuItem];
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool ParseQuery(string equation, out string result)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resultNumber = new DataTable().Compute(equation, null);
|
||||
result = resultNumber.ToString() ?? string.Empty;
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items.ToArray();
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
public sealed partial class SaveCommand : InvokableCommand
|
||||
{
|
||||
public event TypedEventHandler<object, object> SaveRequested;
|
||||
|
||||
public SaveCommand()
|
||||
{
|
||||
Name = Resources.calculator_save_command_name;
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
SaveRequested?.Invoke(this, this);
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
|
||||
internal sealed partial class FallbackCalculatorItem : FallbackCommandItem
|
||||
{
|
||||
private readonly CopyTextCommand _copyCommand = new(string.Empty);
|
||||
|
||||
public FallbackCalculatorItem()
|
||||
: base(new NoOpCommand(), Resources.calculator_title)
|
||||
{
|
||||
Command = _copyCommand;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = Resources.calculator_placeholder_text;
|
||||
Icon = new IconInfo("\ue8ef"); // Calculator
|
||||
}
|
||||
|
||||
public override void UpdateQuery(string query)
|
||||
{
|
||||
if (CalculatorListPage.ParseQuery(query, out var result))
|
||||
{
|
||||
_copyCommand.Text = result;
|
||||
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
|
||||
Title = result;
|
||||
|
||||
// we have to make the subtitle the equation,
|
||||
// so that we will still string match the original query
|
||||
// Otherwise, something like 1+2 will have a title of "3" and not match
|
||||
Subtitle = query;
|
||||
}
|
||||
else
|
||||
{
|
||||
_copyCommand.Text = string.Empty;
|
||||
_copyCommand.Name = string.Empty;
|
||||
Title = string.Empty;
|
||||
Subtitle = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Calc</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Calc.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Calculator.svg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\Calculator.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\Calculator.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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 Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
|
||||
public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ListItem _clipboardHistoryListItem;
|
||||
|
||||
public ClipboardHistoryCommandsProvider()
|
||||
{
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
|
||||
{
|
||||
Title = "Search Clipboard History",
|
||||
Icon = new IconInfo("\xE8C8"), // Copy icon
|
||||
};
|
||||
|
||||
DisplayName = $"Clipboard History";
|
||||
Icon = new IconInfo("\xE8C8"); // Copy icon
|
||||
Id = "Windows.ClipboardHistory";
|
||||
}
|
||||
|
||||
public override IListItem[] TopLevelCommands()
|
||||
{
|
||||
return [_clipboardHistoryListItem];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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 Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
|
||||
internal sealed partial class CopyCommand : InvokableCommand
|
||||
{
|
||||
private readonly ClipboardItem _clipboardItem;
|
||||
private readonly ClipboardFormat _clipboardFormat;
|
||||
|
||||
internal CopyCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Copy";
|
||||
if (clipboardFormat == ClipboardFormat.Text)
|
||||
{
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
}
|
||||
else
|
||||
{
|
||||
Icon = new("\xE8B9"); // Picture icon
|
||||
}
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
return CommandResult.ShowToast("Copied to clipboard");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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 CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
|
||||
internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
private readonly ClipboardItem _clipboardItem;
|
||||
private readonly ClipboardFormat _clipboardFormat;
|
||||
|
||||
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
Name = "Paste";
|
||||
Icon = new("\xE8C8"); // Copy icon
|
||||
}
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
// TODO GH #524: This isn't great - this requires us to have Secret Sauce in
|
||||
// the clipboard extension to be able to manipulate the HWND.
|
||||
// We probably need to put some window manipulation into the API, but
|
||||
// what form that takes is not clear yet.
|
||||
WeakReferenceMessenger.Default.Send<HideWindowMessage>(new());
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetClipboardContent(_clipboardItem, _clipboardFormat);
|
||||
HideWindow();
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
return CommandResult.ShowToast("Pasting");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Data.Html;
|
||||
using Windows.Graphics.Imaging;
|
||||
using Windows.Storage;
|
||||
using Windows.Storage.Streams;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
|
||||
internal static class ClipboardHelper
|
||||
{
|
||||
private static readonly HashSet<string> ImageFileTypes = new(StringComparer.InvariantCultureIgnoreCase) { ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tiff", ".ico", ".svg" };
|
||||
|
||||
private static readonly (string DataFormat, ClipboardFormat ClipboardFormat)[] DataFormats =
|
||||
[
|
||||
(StandardDataFormats.Text, ClipboardFormat.Text),
|
||||
(StandardDataFormats.Html, ClipboardFormat.Html),
|
||||
(StandardDataFormats.Bitmap, ClipboardFormat.Image),
|
||||
];
|
||||
|
||||
internal static async Task<ClipboardFormat> GetAvailableClipboardFormatsAsync(DataPackageView clipboardData)
|
||||
{
|
||||
var availableClipboardFormats = DataFormats.Aggregate(
|
||||
ClipboardFormat.None,
|
||||
(result, formatPair) => clipboardData.Contains(formatPair.DataFormat) ? (result | formatPair.ClipboardFormat) : result);
|
||||
|
||||
if (clipboardData.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var storageItems = await clipboardData.GetStorageItemsAsync();
|
||||
|
||||
if (storageItems.Count == 1 && storageItems.Single() is StorageFile file && ImageFileTypes.Contains(file.FileType))
|
||||
{
|
||||
availableClipboardFormats |= ClipboardFormat.ImageFile;
|
||||
}
|
||||
}
|
||||
|
||||
return availableClipboardFormats;
|
||||
}
|
||||
|
||||
internal static void SetClipboardTextContent(string text)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
{
|
||||
DataPackage output = new();
|
||||
output.SetText(text);
|
||||
try
|
||||
{
|
||||
// Clipboard.SetContentWithOptions(output, null);
|
||||
Clipboard.SetContent(output);
|
||||
Flush();
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied text to clipboard" });
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
ExtensionHost.LogMessage($"Error: {ex.HResult}\n{ex.Source}\n{ex.StackTrace}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool Flush()
|
||||
{
|
||||
// TODO(stefan): For some reason Flush() fails from time to time when directly activated via hotkey.
|
||||
// Calling inside a loop makes it work.
|
||||
// Exception is: The operation is not permitted because the calling application is not the owner of the data on the clipboard.
|
||||
const int maxAttempts = 5;
|
||||
for (var i = 1; i <= maxAttempts; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
Task.Run(Clipboard.Flush).Wait();
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (i == maxAttempts)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage()
|
||||
{
|
||||
Message = $"{nameof(Clipboard)}.{nameof(Flush)}() failed: {ex}",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<bool> FlushAsync() => await Task.Run(Flush);
|
||||
|
||||
internal static async Task SetClipboardFileContentAsync(string fileName)
|
||||
{
|
||||
var storageFile = await StorageFile.GetFileFromPathAsync(fileName);
|
||||
|
||||
DataPackage output = new();
|
||||
output.SetStorageItems([storageFile]);
|
||||
Clipboard.SetContent(output);
|
||||
|
||||
await FlushAsync();
|
||||
}
|
||||
|
||||
internal static void SetClipboardImageContent(RandomAccessStreamReference image)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Copied image to clipboard" });
|
||||
|
||||
if (image is not null)
|
||||
{
|
||||
DataPackage output = new();
|
||||
output.SetBitmap(image);
|
||||
Clipboard.SetContentWithOptions(output, null);
|
||||
|
||||
Flush();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetClipboardContent(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
|
||||
{
|
||||
switch (clipboardFormat)
|
||||
{
|
||||
case ClipboardFormat.Text:
|
||||
if (clipboardItem.Content == null)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" });
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetClipboardTextContent(clipboardItem.Content);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case ClipboardFormat.Image:
|
||||
if (clipboardItem.ImageData == null)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "No valid clipboard content" });
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetClipboardImageContent(clipboardItem.ImageData);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
ExtensionHost.LogMessage(new LogMessage { Message = "Unsupported clipboard format." });
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send a single key event
|
||||
private static void SendSingleKeyboardInput(short keyCode, uint keyStatus)
|
||||
{
|
||||
var ignoreKeyEventFlag = (UIntPtr)0x5555;
|
||||
|
||||
var inputShift = new NativeMethods.INPUT
|
||||
{
|
||||
type = NativeMethods.INPUTTYPE.INPUT_KEYBOARD,
|
||||
data = new NativeMethods.InputUnion
|
||||
{
|
||||
ki = new NativeMethods.KEYBDINPUT
|
||||
{
|
||||
wVk = keyCode,
|
||||
dwFlags = keyStatus,
|
||||
|
||||
// Any keyevent with the extraInfo set to this value will be ignored by the keyboard hook and sent to the system instead.
|
||||
dwExtraInfo = ignoreKeyEventFlag,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var inputs = new NativeMethods.INPUT[] { inputShift };
|
||||
_ = NativeMethods.SendInput(1, inputs, NativeMethods.INPUT.Size);
|
||||
}
|
||||
|
||||
internal static void SendPasteKeyCombination()
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Sending paste keys..." });
|
||||
|
||||
SendSingleKeyboardInput((short)VirtualKey.LeftControl, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.RightControl, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.LeftWindows, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.RightWindows, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.LeftShift, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.RightShift, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.LeftMenu, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.RightMenu, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
|
||||
// Send Ctrl + V
|
||||
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyDown);
|
||||
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyDown);
|
||||
SendSingleKeyboardInput((short)VirtualKey.V, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
SendSingleKeyboardInput((short)VirtualKey.Control, (uint)NativeMethods.KeyEventF.KeyUp);
|
||||
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Paste sent" });
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardTextOrHtmlTextAsync(DataPackageView clipboardData)
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
return await clipboardData.GetTextAsync();
|
||||
}
|
||||
else if (clipboardData.Contains(StandardDataFormats.Html))
|
||||
{
|
||||
var html = await clipboardData.GetHtmlFormatAsync();
|
||||
return HtmlUtilities.ConvertToText(html);
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task<string> GetClipboardHtmlContentAsync(DataPackageView clipboardData) =>
|
||||
clipboardData.Contains(StandardDataFormats.Html) ? await clipboardData.GetHtmlFormatAsync() : string.Empty;
|
||||
|
||||
internal static async Task<SoftwareBitmap?> GetClipboardImageContentAsync(DataPackageView clipboardData)
|
||||
{
|
||||
using var stream = await GetClipboardImageStreamAsync(clipboardData);
|
||||
if (stream != null)
|
||||
{
|
||||
var decoder = await BitmapDecoder.CreateAsync(stream);
|
||||
return await decoder.GetSoftwareBitmapAsync();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<IRandomAccessStream?> GetClipboardImageStreamAsync(DataPackageView clipboardData)
|
||||
{
|
||||
if (clipboardData.Contains(StandardDataFormats.StorageItems))
|
||||
{
|
||||
var storageItems = await clipboardData.GetStorageItemsAsync();
|
||||
var file = storageItems.Count == 1 ? storageItems[0] as StorageFile : null;
|
||||
if (file != null)
|
||||
{
|
||||
return await file.OpenReadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
if (clipboardData.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
var bitmap = await clipboardData.GetBitmapAsync();
|
||||
return await bitmap.OpenReadAsync();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
|
||||
internal static class NativeMethods
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct INPUT
|
||||
{
|
||||
internal INPUTTYPE type;
|
||||
internal InputUnion data;
|
||||
|
||||
internal static int Size => Marshal.SizeOf(typeof(INPUT));
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct InputUnion
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
internal MOUSEINPUT mi;
|
||||
[FieldOffset(0)]
|
||||
internal KEYBDINPUT ki;
|
||||
[FieldOffset(0)]
|
||||
internal HARDWAREINPUT hi;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct MOUSEINPUT
|
||||
{
|
||||
internal int dx;
|
||||
internal int dy;
|
||||
internal int mouseData;
|
||||
internal uint dwFlags;
|
||||
internal uint time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct KEYBDINPUT
|
||||
{
|
||||
internal short wVk;
|
||||
internal short wScan;
|
||||
internal uint dwFlags;
|
||||
internal int time;
|
||||
internal UIntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.NamingRules", "SA1307:Accessible fields should begin with upper-case letter", Justification = "Matching Native Structure")]
|
||||
internal struct HARDWAREINPUT
|
||||
{
|
||||
internal int uMsg;
|
||||
internal short wParamL;
|
||||
internal short wParamH;
|
||||
}
|
||||
|
||||
internal enum INPUTTYPE : uint
|
||||
{
|
||||
INPUT_MOUSE = 0,
|
||||
INPUT_KEYBOARD = 1,
|
||||
INPUT_HARDWARE = 2,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
internal enum KeyEventF
|
||||
{
|
||||
KeyDown = 0x0000,
|
||||
ExtendedKey = 0x0001,
|
||||
KeyUp = 0x0002,
|
||||
Unicode = 0x0004,
|
||||
Scancode = 0x0008,
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
||||
|
||||
[DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||
internal static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct PointInter
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public static explicit operator Point(PointInter point) => new(point.X, point.Y);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
internal static extern bool GetCursorPos(out PointInter lpPoint);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
|
||||
[Flags]
|
||||
public enum ClipboardFormat
|
||||
{
|
||||
None,
|
||||
Text = 1 << 0,
|
||||
Html = 1 << 1,
|
||||
Audio = 1 << 2,
|
||||
Image = 1 << 3,
|
||||
ImageFile = 1 << 4,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
|
||||
public class ClipboardItem
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
|
||||
public required ClipboardHistoryItem Item { get; set; }
|
||||
|
||||
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
|
||||
|
||||
public RandomAccessStreamReference? ImageData { get; set; }
|
||||
|
||||
public string GetDataType()
|
||||
{
|
||||
// Check if there is valid image data
|
||||
if (IsImage)
|
||||
{
|
||||
return "Image";
|
||||
}
|
||||
|
||||
// Check if there is valid text content
|
||||
return IsText ? "Text" : "Unknown";
|
||||
}
|
||||
|
||||
[MemberNotNullWhen(true, nameof(ImageData))]
|
||||
private bool IsImage => ImageData != null;
|
||||
|
||||
[MemberNotNullWhen(true, nameof(Content))]
|
||||
private bool IsText => !string.IsNullOrEmpty(Content);
|
||||
|
||||
public static List<string> ShiftLinesLeft(List<string> lines)
|
||||
{
|
||||
// Determine the minimum leading whitespace
|
||||
var minLeadingWhitespace = lines
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
|
||||
|
||||
// Check if all lines have at least that much leading whitespace
|
||||
if (lines.Any(line => line.TakeWhile(char.IsWhiteSpace).Count() < minLeadingWhitespace))
|
||||
{
|
||||
return lines; // Return the original lines if any line doesn't have enough leading whitespace
|
||||
}
|
||||
|
||||
// Remove the minimum leading whitespace from each line
|
||||
var shiftedLines = lines.Select(line => line.Substring(minLeadingWhitespace)).ToList();
|
||||
|
||||
return shiftedLines;
|
||||
}
|
||||
|
||||
public static List<string> StripLeadingWhitespace(List<string> lines)
|
||||
{
|
||||
// Determine the minimum leading whitespace
|
||||
var minLeadingWhitespace = lines
|
||||
.Min(line => line.TakeWhile(char.IsWhiteSpace).Count());
|
||||
|
||||
// Remove the minimum leading whitespace from each line
|
||||
var shiftedLines = lines.Select(line =>
|
||||
line.Length >= minLeadingWhitespace
|
||||
? line.Substring(minLeadingWhitespace)
|
||||
: line).ToList();
|
||||
|
||||
return shiftedLines;
|
||||
}
|
||||
|
||||
public ListItem ToListItem()
|
||||
{
|
||||
ListItem listItem;
|
||||
|
||||
List<DetailsElement> metadata = [];
|
||||
metadata.Add(new DetailsElement()
|
||||
{
|
||||
Key = "Copied on",
|
||||
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
||||
});
|
||||
|
||||
if (IsImage)
|
||||
{
|
||||
var iconData = new IconData(ImageData);
|
||||
var heroImage = new IconInfo(iconData, iconData);
|
||||
listItem = new(new CopyCommand(this, ClipboardFormat.Image))
|
||||
{
|
||||
// Placeholder subtitle as there’s no BitmapImage dimensions to retrieve
|
||||
Title = "Image Data",
|
||||
Details = new Details()
|
||||
{
|
||||
HeroImage = heroImage,
|
||||
Title = GetDataType(),
|
||||
Body = Timestamp.ToString(CultureInfo.InvariantCulture),
|
||||
Metadata = metadata.ToArray(),
|
||||
},
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image))
|
||||
],
|
||||
};
|
||||
}
|
||||
else if (IsText)
|
||||
{
|
||||
var splitContent = Content.Split("\n");
|
||||
var head = splitContent.AsSpan(0, Math.Min(3, splitContent.Length)).ToArray().ToList();
|
||||
var preview2 = string.Join(
|
||||
"\n",
|
||||
StripLeadingWhitespace(head));
|
||||
|
||||
listItem = new(new CopyCommand(this, ClipboardFormat.Text))
|
||||
{
|
||||
Title = preview2,
|
||||
|
||||
Details = new Details
|
||||
{
|
||||
Title = GetDataType(),
|
||||
Body = $"```text\n{Content}\n```",
|
||||
Metadata = metadata.ToArray(),
|
||||
},
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)),
|
||||
],
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
listItem = new(new NoOpCommand())
|
||||
{
|
||||
Title = "Unknown",
|
||||
Subtitle = GetDataType(),
|
||||
Details = new Details { Title = GetDataType() },
|
||||
};
|
||||
}
|
||||
|
||||
return listItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Win32;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
||||
|
||||
internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
||||
private readonly string _defaultIconPath;
|
||||
|
||||
public ClipboardHistoryListPage()
|
||||
{
|
||||
clipboardHistory = [];
|
||||
_defaultIconPath = string.Empty;
|
||||
Icon = new("\uF0E3"); // ClipboardList icon
|
||||
Name = "Clipboard History";
|
||||
Id = "com.microsoft.cmdpal.clipboardHistory";
|
||||
ShowDetails = true;
|
||||
|
||||
Clipboard.HistoryChanged += TrackClipboardHistoryChanged_EventHandler;
|
||||
}
|
||||
|
||||
private void TrackClipboardHistoryChanged_EventHandler(object? sender, ClipboardHistoryChangedEventArgs? e) => RaiseItemsChanged(0);
|
||||
|
||||
private bool IsClipboardHistoryEnabled()
|
||||
{
|
||||
var registryKey = @"HKEY_CURRENT_USER\Software\Microsoft\Clipboard\";
|
||||
try
|
||||
{
|
||||
var enableClipboardHistory = (int)(Registry.GetValue(registryKey, "EnableClipboardHistory", false) ?? 0);
|
||||
return enableClipboardHistory != 0;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsClipboardHistoryDisabledByGPO()
|
||||
{
|
||||
var registryKey = @"HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Windows\System\";
|
||||
try
|
||||
{
|
||||
var allowClipboardHistory = Registry.GetValue(registryKey, "AllowClipboardHistory", null);
|
||||
return allowClipboardHistory != null ? (int)allowClipboardHistory == 0 : false;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadClipboardHistoryAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
List<ClipboardItem> items = [];
|
||||
|
||||
if (!Clipboard.IsHistoryEnabled())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var historyItems = await Clipboard.GetHistoryItemsAsync();
|
||||
if (historyItems.Status != ClipboardHistoryItemsResultStatus.Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in historyItems.Items)
|
||||
{
|
||||
if (item.Content.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
var text = await item.Content.GetTextAsync();
|
||||
items.Add(new ClipboardItem { Content = text, Item = item });
|
||||
}
|
||||
else if (item.Content.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
items.Add(new ClipboardItem { Item = item });
|
||||
}
|
||||
}
|
||||
|
||||
clipboardHistory.Clear();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item.Item.Content.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
var imageReceived = await item.Item.Content.GetBitmapAsync();
|
||||
|
||||
if (imageReceived != null)
|
||||
{
|
||||
item.ImageData = imageReceived;
|
||||
}
|
||||
}
|
||||
|
||||
clipboardHistory.Add(item);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// TODO GH #108 We need to figure out some logging
|
||||
// Logger.LogError("Loading clipboard history failed", ex);
|
||||
ExtensionHost.ShowStatus(new StatusMessage() { Message = "Loading clipboard history failed", State = MessageState.Error }, StatusContext.Page);
|
||||
ExtensionHost.LogMessage(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadClipboardHistoryInSTA()
|
||||
{
|
||||
// https://github.com/microsoft/windows-rs/issues/317
|
||||
// Clipboard API needs to be called in STA or it
|
||||
// hangs.
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
var t = LoadClipboardHistoryAsync();
|
||||
t.ConfigureAwait(false);
|
||||
t.Wait();
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
}
|
||||
|
||||
private ListItem[] GetClipboardHistoryListItems()
|
||||
{
|
||||
LoadClipboardHistoryInSTA();
|
||||
List<ListItem> listItems = [];
|
||||
for (var i = 0; i < clipboardHistory.Count; i++)
|
||||
{
|
||||
var item = clipboardHistory[i];
|
||||
if (item != null)
|
||||
{
|
||||
listItems.Add(item.ToListItem());
|
||||
}
|
||||
}
|
||||
|
||||
return listItems.ToArray();
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => GetClipboardHistoryListItems();
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,93 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1825_18138)">
|
||||
<path d="M8.3125 3L7.47871 1.89974C7.26684 1.62016 6.9931 1.39344 6.67896 1.23734C6.36481 1.08125 6.01879 1.00001 5.668 1H1C0.734784 1 0.48043 1.10536 0.292893 1.29289C0.105357 1.48043 0 1.73478 0 2L0 12C0.00155124 12.53 0.212763 13.0378 0.5875 13.4125C0.962237 13.7872 1.47004 13.9984 2 14H14C14.53 13.9984 15.0378 13.7872 15.4125 13.4125C15.7872 13.0378 15.9984 12.53 16 12V4C16 3.73478 15.8946 3.48043 15.7071 3.29289C15.5196 3.10536 15.2652 3 15 3H8.3125Z" fill="url(#paint0_linear_1825_18138)"/>
|
||||
<mask id="mask0_1825_18138" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="1" width="16" height="13">
|
||||
<path d="M8.3125 3L7.47871 1.89974C7.26684 1.62016 6.9931 1.39344 6.67896 1.23734C6.36481 1.08125 6.01879 1.00001 5.668 1H1C0.734784 1 0.48043 1.10536 0.292893 1.29289C0.105357 1.48043 0 1.73478 0 2L0 12C0.00155124 12.53 0.212763 13.0378 0.5875 13.4125C0.962237 13.7872 1.47004 13.9984 2 14H14C14.53 13.9984 15.0378 13.7872 15.4125 13.4125C15.7872 13.0378 15.9984 12.53 16 12V4C16 3.73478 15.8946 3.48043 15.7071 3.29289C15.5196 3.10536 15.2652 3 15 3H8.3125Z" fill="url(#paint1_linear_1825_18138)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_1825_18138)">
|
||||
<g filter="url(#filter0_dd_1825_18138)">
|
||||
<path d="M14.9985 5.00187H10L8 5.00172C7.62917 5.19449 6.41839 4.9973 6.00047 5.00171H1.00151C0.868112 4.99367 0.734525 5.01401 0.609573 5.06142C0.484622 5.10882 0.37115 5.18219 0.276655 5.27669C0.18216 5.37119 0.108792 5.48467 0.061398 5.60962C0.0140044 5.73458 -0.00633622 5.86817 0.00172002 6.00156V13.9986C-0.00634109 14.132 0.0139961 14.2656 0.0613881 14.3905C0.10878 14.5155 0.182148 14.6289 0.276644 14.7234C0.37114 14.8179 0.484614 14.8913 0.609568 14.9387C0.734521 14.9861 0.868111 15.0065 1.00151 14.9984H14.9985C15.1319 15.0065 15.2655 14.9861 15.3904 14.9387C15.5154 14.8913 15.6289 14.8179 15.7234 14.7234C15.8178 14.6289 15.8912 14.5155 15.9386 14.3905C15.986 14.2655 16.0064 14.132 15.9983 13.9986V6.00171C16.0064 5.86832 15.986 5.73473 15.9386 5.60977C15.8912 5.48481 15.8179 5.37134 15.7234 5.27683C15.6289 5.18233 15.5154 5.10896 15.3905 5.06156C15.2655 5.01416 15.1319 4.99382 14.9985 5.00187Z" fill="url(#paint2_linear_1825_18138)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M14.9985 4.00172H8.2L7.20022 4.70161C6.82939 4.89438 6.41839 4.99714 6.00047 5.00156H1.00151C0.868112 4.99351 0.734525 5.01386 0.609573 5.06126C0.484622 5.10866 0.37115 5.18203 0.276655 5.27654C0.18216 5.37104 0.108792 5.48451 0.061398 5.60947C0.0140044 5.73442 -0.00633622 5.86801 0.00172002 6.00141V13.9984C-0.00634109 14.1318 0.0139961 14.2654 0.0613881 14.3904C0.10878 14.5153 0.182148 14.6288 0.276644 14.7233C0.37114 14.8178 0.484614 14.8912 0.609568 14.9386C0.734521 14.986 0.868111 15.0063 1.00151 14.9983H14.9985C15.1319 15.0063 15.2655 14.9859 15.3904 14.9385C15.5154 14.8911 15.6289 14.8178 15.7234 14.7233C15.8178 14.6288 15.8912 14.5153 15.9386 14.3903C15.986 14.2654 16.0064 14.1318 15.9983 13.9984V5.00156C16.0064 4.86816 15.986 4.73457 15.9386 4.60961C15.8912 4.48466 15.8179 4.37118 15.7234 4.27668C15.6289 4.18218 15.5154 4.1088 15.3905 4.0614C15.2655 4.01401 15.1319 3.99366 14.9985 4.00172H14.9985Z" fill="url(#paint3_linear_1825_18138)"/>
|
||||
<path opacity="0.25" d="M15.7017 4.30036C15.6137 4.20263 15.5056 4.1252 15.3847 4.0734C15.2638 4.0216 15.1332 3.99668 15.0017 4.00036H8.20172L7.20172 4.70036C6.83081 4.89316 6.41972 4.99594 6.00172 5.00036H1.00172C0.868297 4.9923 0.734682 5.01264 0.609705 5.06005C0.484727 5.10745 0.371231 5.18083 0.276715 5.27535C0.182198 5.36987 0.108814 5.48336 0.0614098 5.60834C0.0140056 5.73332 -0.00633928 5.86693 0.00171929 6.00036V7.00036C-0.00634091 6.86693 0.0140029 6.73332 0.0614065 6.60834C0.10881 6.48336 0.182194 6.36986 0.276711 6.27535C0.371227 6.18083 0.484724 6.10745 0.609703 6.06004C0.734681 6.01264 0.868296 5.9923 1.00172 6.00036H6.00172C6.60283 5.98827 7.18978 5.81563 7.70172 5.50036L8.50172 5.00036H15.0017C15.1351 4.9923 15.2688 5.01265 15.3937 5.06006C15.5187 5.10746 15.6322 5.18085 15.7267 5.27536C15.8212 5.36988 15.8946 5.48337 15.942 5.60835C15.9894 5.73332 16.0098 5.86693 16.0017 6.00036V5.00036C16.0054 4.86891 15.9805 4.73823 15.9287 4.61736C15.8769 4.49649 15.7995 4.38833 15.7017 4.30036Z" fill="url(#paint4_linear_1825_18138)"/>
|
||||
<mask id="mask1_1825_18138" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="4" width="16" height="11">
|
||||
<path d="M14.9985 4.00172H8.2L7.20022 4.70161C6.82939 4.89438 6.41839 4.99714 6.00047 5.00156H1.00151C0.868112 4.99351 0.734525 5.01386 0.609573 5.06126C0.484622 5.10866 0.37115 5.18203 0.276655 5.27654C0.18216 5.37104 0.108792 5.48451 0.061398 5.60947C0.0140044 5.73442 -0.00633622 5.86801 0.00172002 6.00141V13.9984C-0.00634109 14.1318 0.0139961 14.2654 0.0613881 14.3904C0.10878 14.5153 0.182148 14.6288 0.276644 14.7233C0.37114 14.8178 0.484614 14.8912 0.609568 14.9386C0.734521 14.986 0.868111 15.0063 1.00151 14.9983H14.9985C15.1319 15.0063 15.2655 14.9859 15.3904 14.9385C15.5154 14.8911 15.6289 14.8178 15.7234 14.7233C15.8178 14.6288 15.8912 14.5153 15.9386 14.3903C15.986 14.2654 16.0064 14.1318 15.9983 13.9984V5.00156C16.0064 4.86816 15.986 4.73457 15.9386 4.60961C15.8912 4.48466 15.8179 4.37118 15.7234 4.27668C15.6289 4.18218 15.5154 4.1088 15.3905 4.0614C15.2655 4.01401 15.1319 3.99366 14.9985 4.00172H14.9985Z" fill="url(#paint5_linear_1825_18138)"/>
|
||||
</mask>
|
||||
<g mask="url(#mask1_1825_18138)">
|
||||
<g filter="url(#filter1_dd_1825_18138)">
|
||||
<path d="M5 10H11C11.5304 10 12.0391 10.2107 12.4142 10.5858C12.7893 10.9609 13 11.4696 13 12V15H3V12C3 11.4696 3.21071 10.9609 3.58579 10.5858C3.96086 10.2107 4.46957 10 5 10Z" fill="url(#paint6_linear_1825_18138)"/>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M5 10H11C11.5304 10 12.0391 10.2107 12.4142 10.5858C12.7893 10.9609 13 11.4696 13 12V15H3V12C3 11.4696 3.21071 10.9609 3.58579 10.5858C3.96086 10.2107 4.46957 10 5 10Z" fill="url(#paint7_linear_1825_18138)"/>
|
||||
<path d="M10.5 12H5.5C5.22386 12 5 12.2239 5 12.5C5 12.7761 5.22386 13 5.5 13H10.5C10.7761 13 11 12.7761 11 12.5C11 12.2239 10.7761 12 10.5 12Z" fill="#114A8B"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd_1825_18138" x="-1" y="4" width="18" height="12" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.166667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_18138"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1825_18138" result="effect2_dropShadow_1825_18138"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_18138" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd_1825_18138" x="2" y="9" width="12" height="7" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.166667"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1825_18138"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_1825_18138" result="effect2_dropShadow_1825_18138"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow_1825_18138" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1825_18138" x1="11.4683" y1="14.139" x2="4.1515" y2="1.4659" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.1135" stop-color="#D18B00"/>
|
||||
<stop offset="0.6162" stop-color="#E09F00"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1825_18138" x1="11.4683" y1="14.139" x2="4.1515" y2="1.4659" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.1135" stop-color="#D18B00"/>
|
||||
<stop offset="0.6162" stop-color="#E09F00"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1825_18138" x1="12.1767" y1="16.7345" x2="4.48067" y2="3.40451" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F5B300"/>
|
||||
<stop offset="0.5" stop-color="#FFCB3C"/>
|
||||
<stop offset="1" stop-color="#FFD762"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1825_18138" x1="12.1767" y1="16.7343" x2="4.48067" y2="3.40435" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F5B300"/>
|
||||
<stop offset="0.5" stop-color="#FFCB3C"/>
|
||||
<stop offset="1" stop-color="#FFD762"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_1825_18138" x1="0.430309" y1="5.50017" x2="16.0017" y2="5.50017" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="white"/>
|
||||
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_1825_18138" x1="12.1767" y1="16.7343" x2="4.48067" y2="3.40435" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F5B300"/>
|
||||
<stop offset="0.5" stop-color="#FFCB3C"/>
|
||||
<stop offset="1" stop-color="#FFD762"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_1825_18138" x1="8.30516" y1="15.1422" x2="7.36003" y2="9.7821" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0062B4"/>
|
||||
<stop offset="1" stop-color="#1493DF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_1825_18138" x1="8.30516" y1="15.1422" x2="7.36003" y2="9.7821" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0062B4"/>
|
||||
<stop offset="1" stop-color="#1493DF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1825_18138">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.9 KiB |
@@ -0,0 +1,38 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
internal sealed partial class CopyPathCommand : InvokableCommand
|
||||
{
|
||||
private readonly IndexerItem _item;
|
||||
|
||||
internal CopyPathCommand(IndexerItem item)
|
||||
{
|
||||
this._item = item;
|
||||
this.Name = Resources.Indexer_Command_CopyPath;
|
||||
this.Icon = new IconInfo("\uE8c8");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
var dataPackage = new DataPackage();
|
||||
dataPackage.SetText(_item.FullPath);
|
||||
Clipboard.SetContent(dataPackage);
|
||||
Clipboard.Flush();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
internal sealed partial class OpenFileCommand : InvokableCommand
|
||||
{
|
||||
private readonly IndexerItem _item;
|
||||
|
||||
internal OpenFileCommand(IndexerItem item)
|
||||
{
|
||||
this._item = item;
|
||||
this.Name = Resources.Indexer_Command_OpenFile;
|
||||
this.Icon = Icons.OpenFile;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo.FileName = _item.FullPath;
|
||||
process.StartInfo.UseShellExecute = true;
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
Logger.LogError($"Unable to open {_item.FullPath}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
internal sealed partial class OpenInConsoleCommand : InvokableCommand
|
||||
{
|
||||
private readonly IndexerItem _item;
|
||||
|
||||
internal OpenInConsoleCommand(IndexerItem item)
|
||||
{
|
||||
this._item = item;
|
||||
this.Name = Resources.Indexer_Command_OpenPathInConsole;
|
||||
this.Icon = new IconInfo("\uE756");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo.WorkingDirectory = Path.GetDirectoryName(_item.FullPath);
|
||||
process.StartInfo.FileName = "cmd.exe";
|
||||
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
Logger.LogError($"Unable to open {_item.FullPath}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 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;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Native;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
internal sealed partial class OpenPropertiesCommand : InvokableCommand
|
||||
{
|
||||
private readonly IndexerItem _item;
|
||||
|
||||
private static unsafe bool ShowFileProperties(string filename)
|
||||
{
|
||||
var propertiesPtr = Marshal.StringToHGlobalUni("properties");
|
||||
var filenamePtr = Marshal.StringToHGlobalUni(filename);
|
||||
|
||||
try
|
||||
{
|
||||
var filenamePCWSTR = new PCWSTR((char*)filenamePtr);
|
||||
var propertiesPCWSTR = new PCWSTR((char*)propertiesPtr);
|
||||
|
||||
var info = new SHELLEXECUTEINFOW
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<SHELLEXECUTEINFOW>(),
|
||||
lpVerb = propertiesPCWSTR,
|
||||
lpFile = filenamePCWSTR,
|
||||
nShow = (int)SHOW_WINDOW_CMD.SW_SHOW,
|
||||
fMask = NativeHelpers.SEEMASKINVOKEIDLIST,
|
||||
};
|
||||
|
||||
return PInvoke.ShellExecuteEx(ref info);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(filenamePtr);
|
||||
Marshal.FreeHGlobal(propertiesPtr);
|
||||
}
|
||||
}
|
||||
|
||||
internal OpenPropertiesCommand(IndexerItem item)
|
||||
{
|
||||
this._item = item;
|
||||
this.Name = Resources.Indexer_Command_OpenProperties;
|
||||
this.Icon = new IconInfo("\uE90F");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowFileProperties(_item.FullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Error showing file properties: ", ex);
|
||||
}
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Native;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
|
||||
internal sealed partial class OpenWithCommand : InvokableCommand
|
||||
{
|
||||
private readonly IndexerItem _item;
|
||||
|
||||
private static unsafe bool OpenWith(string filename)
|
||||
{
|
||||
var filenamePtr = Marshal.StringToHGlobalUni(filename);
|
||||
var verbPtr = Marshal.StringToHGlobalUni("openas");
|
||||
|
||||
try
|
||||
{
|
||||
var filenamePCWSTR = new PCWSTR((char*)filenamePtr);
|
||||
var verbPCWSTR = new PCWSTR((char*)verbPtr);
|
||||
|
||||
var info = new SHELLEXECUTEINFOW
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf<SHELLEXECUTEINFOW>(),
|
||||
lpVerb = verbPCWSTR,
|
||||
lpFile = filenamePCWSTR,
|
||||
nShow = (int)SHOW_WINDOW_CMD.SW_SHOWNORMAL,
|
||||
fMask = NativeHelpers.SEEMASKINVOKEIDLIST,
|
||||
};
|
||||
|
||||
return PInvoke.ShellExecuteEx(ref info);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(filenamePtr);
|
||||
Marshal.FreeHGlobal(verbPtr);
|
||||
}
|
||||
}
|
||||
|
||||
internal OpenWithCommand(IndexerItem item)
|
||||
{
|
||||
this._item = item;
|
||||
this.Name = Resources.Indexer_Command_OpenWith;
|
||||
this.Icon = new IconInfo("\uE7AC");
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
OpenWith(_item.FullPath);
|
||||
|
||||
return CommandResult.GoHome();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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.IO;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
|
||||
internal sealed class IndexerItem
|
||||
{
|
||||
internal string FullPath { get; init; }
|
||||
|
||||
internal string FileName { get; init; }
|
||||
|
||||
internal bool IsDirectory()
|
||||
{
|
||||
if (!Path.Exists(FullPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attr = File.GetAttributes(FullPath);
|
||||
|
||||
// detect whether it is a directory or file
|
||||
return (attr & FileAttributes.Directory) == FileAttributes.Directory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
|
||||
internal sealed partial class IndexerListItem : ListItem
|
||||
{
|
||||
internal string FilePath { get; private set; }
|
||||
|
||||
public IndexerListItem(
|
||||
IndexerItem indexerItem,
|
||||
IncludeBrowseCommand browseByDefault = IncludeBrowseCommand.Include)
|
||||
: base(new OpenFileCommand(indexerItem))
|
||||
{
|
||||
FilePath = indexerItem.FullPath;
|
||||
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
List<CommandContextItem> context = [];
|
||||
if (indexerItem.IsDirectory())
|
||||
{
|
||||
var directoryPage = new DirectoryPage(indexerItem.FullPath);
|
||||
if (browseByDefault == IncludeBrowseCommand.AsDefault)
|
||||
{
|
||||
// Swap the open file command into the context menu
|
||||
context.Add(new CommandContextItem(Command));
|
||||
Command = directoryPage;
|
||||
}
|
||||
else if (browseByDefault == IncludeBrowseCommand.Include)
|
||||
{
|
||||
context.Add(new CommandContextItem(directoryPage));
|
||||
}
|
||||
}
|
||||
|
||||
MoreCommands = [
|
||||
..context,
|
||||
new CommandContextItem(new OpenWithCommand(indexerItem)),
|
||||
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
|
||||
new CommandContextItem(new CopyPathCommand(indexerItem)),
|
||||
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
|
||||
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
internal enum IncludeBrowseCommand
|
||||
{
|
||||
AsDefault = 0,
|
||||
Include = 1,
|
||||
Exclude = 2,
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 ManagedCommon;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.System.Com;
|
||||
using Windows.Win32.System.Search;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
|
||||
|
||||
internal static class DataSourceManager
|
||||
{
|
||||
private static readonly Guid CLSIDCollatorDataSource = new("9E175B8B-F52A-11D8-B9A5-505054503030");
|
||||
|
||||
private static IDBInitialize _dataSource;
|
||||
|
||||
public static IDBInitialize GetDataSource()
|
||||
{
|
||||
if (_dataSource == null)
|
||||
{
|
||||
InitializeDataSource();
|
||||
}
|
||||
|
||||
return _dataSource;
|
||||
}
|
||||
|
||||
private static bool InitializeDataSource()
|
||||
{
|
||||
var hr = PInvoke.CoCreateInstance(CLSIDCollatorDataSource, null, CLSCTX.CLSCTX_INPROC_SERVER, typeof(IDBInitialize).GUID, out var dataSourceObj);
|
||||
if (hr != 0)
|
||||
{
|
||||
Logger.LogError("CoCreateInstance failed: " + hr);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dataSourceObj == null)
|
||||
{
|
||||
Logger.LogError("CoCreateInstance failed: dataSourceObj is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
_dataSource = (IDBInitialize)dataSourceObj;
|
||||
_dataSource.Initialize();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using Windows.Win32.Storage.IndexServer;
|
||||
using Windows.Win32.System.Com.StructuredStorage;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.OleDB;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct DBPROP
|
||||
{
|
||||
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
|
||||
public uint dwPropertyID;
|
||||
public uint dwOptions;
|
||||
public uint dwStatus;
|
||||
public DBID colid;
|
||||
public PROPVARIANT vValue;
|
||||
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.CmdPal.Ext.Indexer.Indexer.OleDB;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DBPROPIDSET
|
||||
{
|
||||
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
|
||||
public IntPtr rgPropertyIDs; // Pointer to array of property IDs
|
||||
public uint cPropertyIDs; // Number of properties in array
|
||||
public Guid guidPropertySet; // GUID of the property set
|
||||
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.CmdPal.Ext.Indexer.Indexer.OleDB;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct DBPROPSET
|
||||
{
|
||||
#pragma warning disable SA1307 // Accessible fields should begin with upper-case letter
|
||||
public IntPtr rgProperties; // Pointer to an array of DBPROP
|
||||
public uint cProperties; // Number of properties in the array
|
||||
public Guid guidPropertySet; // GUID of the property set
|
||||
#pragma warning restore SA1307 // Accessible fields should begin with upper-case letter
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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.CmdPal.Ext.Indexer.Indexer.OleDB;
|
||||
|
||||
[ComImport]
|
||||
[Guid("0c733a7c-2a1c-11ce-ade5-00aa0044773d")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IRowset
|
||||
{
|
||||
[PreserveSig]
|
||||
int AddRefRows(
|
||||
uint cRows,
|
||||
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows,
|
||||
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts,
|
||||
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus);
|
||||
|
||||
[PreserveSig]
|
||||
int GetData(
|
||||
IntPtr hRow,
|
||||
IntPtr hAccessor,
|
||||
IntPtr pData);
|
||||
|
||||
[PreserveSig]
|
||||
int GetNextRows(
|
||||
IntPtr hReserved,
|
||||
long lRowsOffset,
|
||||
long cRows,
|
||||
out uint pcRowsObtained,
|
||||
out IntPtr prghRows);
|
||||
|
||||
[PreserveSig]
|
||||
int ReleaseRows(
|
||||
uint cRows,
|
||||
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] IntPtr[] rghRows,
|
||||
IntPtr rgRowOptions,
|
||||
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] uint[] rgRefCounts,
|
||||
[Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] int[] rgRowStatus);
|
||||
}
|
||||
@@ -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.CmdPal.Ext.Indexer.Indexer.OleDB;
|
||||
|
||||
[ComImport]
|
||||
[Guid("0C733A55-2A1C-11CE-ADE5-00AA0044773D")]
|
||||
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
public interface IRowsetInfo
|
||||
{
|
||||
[PreserveSig]
|
||||
int GetProperties(
|
||||
uint cPropertyIDSets,
|
||||
[In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)] DBPROPIDSET[] rgPropertyIDSets,
|
||||
out ulong pcPropertySets,
|
||||
out IntPtr prgPropertySets);
|
||||
|
||||
[PreserveSig]
|
||||
int GetReferencedRowset(
|
||||
uint iOrdinal,
|
||||
[In] ref Guid riid,
|
||||
[Out, MarshalAs(UnmanagedType.Interface)] out object ppReferencedRowset);
|
||||
|
||||
[PreserveSig]
|
||||
int GetSpecification(
|
||||
[In] ref Guid riid,
|
||||
[Out, MarshalAs(UnmanagedType.Interface)] out object ppSpecification);
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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 ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Native;
|
||||
using Windows.Win32.System.Com;
|
||||
using Windows.Win32.System.Com.StructuredStorage;
|
||||
using Windows.Win32.UI.Shell.PropertiesSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Indexer;
|
||||
|
||||
internal sealed class SearchResult
|
||||
{
|
||||
public string ItemDisplayName { get; init; }
|
||||
|
||||
public string ItemUrl { get; init; }
|
||||
|
||||
public string LaunchUri { get; init; }
|
||||
|
||||
public bool IsFolder { get; init; }
|
||||
|
||||
public SearchResult(string name, string url, string filePath, bool isFolder)
|
||||
{
|
||||
ItemDisplayName = name;
|
||||
ItemUrl = url;
|
||||
IsFolder = isFolder;
|
||||
|
||||
if (LaunchUri == null || LaunchUri.Length == 0)
|
||||
{
|
||||
// Launch the file with the default app, so use the file path
|
||||
LaunchUri = filePath;
|
||||
}
|
||||
}
|
||||
|
||||
public static unsafe SearchResult Create(IPropertyStore propStore)
|
||||
{
|
||||
try
|
||||
{
|
||||
var key = NativeHelpers.PropertyKeys.PKEYItemNameDisplay;
|
||||
propStore.GetValue(&key, out var itemNameDisplay);
|
||||
|
||||
key = NativeHelpers.PropertyKeys.PKEYItemUrl;
|
||||
propStore.GetValue(&key, out var itemUrl);
|
||||
|
||||
key = NativeHelpers.PropertyKeys.PKEYKindText;
|
||||
propStore.GetValue(&key, out var kindText);
|
||||
|
||||
var filePath = GetFilePath(ref itemUrl);
|
||||
var isFolder = IsFoder(ref kindText);
|
||||
|
||||
// Create the actual result object
|
||||
var searchResult = new SearchResult(
|
||||
GetStringFromPropVariant(ref itemNameDisplay),
|
||||
GetStringFromPropVariant(ref itemUrl),
|
||||
filePath,
|
||||
isFolder);
|
||||
|
||||
return searchResult;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get property values from propStore.", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsFoder(ref PROPVARIANT kindText)
|
||||
{
|
||||
var kindString = GetStringFromPropVariant(ref kindText);
|
||||
return string.Equals(kindString, "Folder", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string GetFilePath(ref PROPVARIANT itemUrl)
|
||||
{
|
||||
var filePath = GetStringFromPropVariant(ref itemUrl);
|
||||
filePath = UrlToFilePathConverter.Convert(filePath);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private static string GetStringFromPropVariant(ref PROPVARIANT propVariant)
|
||||
{
|
||||
if (propVariant.Anonymous.Anonymous.vt == VARENUM.VT_LPWSTR)
|
||||
{
|
||||
var pwszVal = propVariant.Anonymous.Anonymous.Anonymous.pwszVal;
|
||||
if (pwszVal != null)
|
||||
{
|
||||
return pwszVal.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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.Globalization;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Indexer.SystemSearch;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
|
||||
|
||||
internal sealed class QueryStringBuilder
|
||||
{
|
||||
private const string Properties = "System.ItemUrl, System.ItemNameDisplay, path, System.Search.EntryID, System.Kind, System.KindText";
|
||||
private const string SystemIndex = "SystemIndex";
|
||||
private const string ScopeFileConditions = "SCOPE='file:'";
|
||||
private const string OrderConditions = "System.DateModified DESC";
|
||||
private const string SelectQueryWithScope = "SELECT " + Properties + " FROM " + SystemIndex + " WHERE (" + ScopeFileConditions + ")";
|
||||
private const string SelectQueryWithScopeAndOrderConditions = SelectQueryWithScope + " ORDER BY " + OrderConditions;
|
||||
|
||||
private static ISearchQueryHelper queryHelper;
|
||||
|
||||
public static string GeneratePrimingQuery() => SelectQueryWithScopeAndOrderConditions;
|
||||
|
||||
public static string GenerateQuery(string searchText, uint whereId)
|
||||
{
|
||||
if (queryHelper == null)
|
||||
{
|
||||
var searchManager = new CSearchManager();
|
||||
ISearchCatalogManager catalogManager = searchManager.GetCatalog(SystemIndex);
|
||||
queryHelper = catalogManager.GetQueryHelper();
|
||||
|
||||
queryHelper.QuerySelectColumns = Properties;
|
||||
queryHelper.QueryContentProperties = "System.FileName";
|
||||
queryHelper.QuerySorting = OrderConditions;
|
||||
}
|
||||
|
||||
queryHelper.QueryWhereRestrictions = "AND " + ScopeFileConditions + "AND ReuseWhere(" + whereId.ToString(CultureInfo.InvariantCulture) + ")";
|
||||
return queryHelper.GenerateSQLFromUserQuery(searchText);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Indexer.Utils;
|
||||
|
||||
public class UrlToFilePathConverter
|
||||
{
|
||||
public static string Convert(string url)
|
||||
{
|
||||
var result = url.Replace('/', '\\'); // replace all '/' to '\'
|
||||
|
||||
var fileProtocolString = "file:";
|
||||
var indexProtocolFound = url.IndexOf(fileProtocolString, StringComparison.CurrentCultureIgnoreCase);
|
||||
|
||||
if (indexProtocolFound != -1 && (indexProtocolFound + fileProtocolString.Length) < url.Length)
|
||||
{
|
||||
result = result[(indexProtocolFound + fileProtocolString.Length)..];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Indexer</RootNamespace>
|
||||
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\FileExplorer.svg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\FileExplorer.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\FileExplorer.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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 Windows.Win32.UI.Shell.PropertiesSystem;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer.Native;
|
||||
|
||||
internal sealed class NativeHelpers
|
||||
{
|
||||
public const uint SEEMASKINVOKEIDLIST = 12;
|
||||
|
||||
internal static class PropertyKeys
|
||||
{
|
||||
public static readonly PROPERTYKEY PKEYItemNameDisplay = new() { fmtid = new System.Guid("B725F130-47EF-101A-A5F1-02608C9EEBAC"), pid = 10 };
|
||||
public static readonly PROPERTYKEY PKEYItemUrl = new() { fmtid = new System.Guid("49691C90-7E17-101A-A91C-08002B2ECDA9"), pid = 9 };
|
||||
public static readonly PROPERTYKEY PKEYKindText = new() { fmtid = new System.Guid("F04BEF95-C585-4197-A2B7-DF46FDC9EE6D"), pid = 100 };
|
||||
}
|
||||
|
||||
internal static class OleDb
|
||||
{
|
||||
public static readonly Guid DbGuidDefault = new("C8B521FB-5CF3-11CE-ADE5-00AA0044773D");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
#nullable enable
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
/// <summary>
|
||||
/// This is almost more of just a sample than anything.
|
||||
/// This is one singular page for switching.
|
||||
/// </summary>
|
||||
public sealed partial class DirectoryExplorePage : DynamicListPage
|
||||
{
|
||||
private string _path;
|
||||
private List<ExploreListItem>? _directoryContents;
|
||||
private List<ExploreListItem>? _filteredContents;
|
||||
|
||||
public DirectoryExplorePage(string path)
|
||||
{
|
||||
_path = path;
|
||||
Icon = Icons.FileExplorer;
|
||||
Name = Resources.Indexer_Command_Browse;
|
||||
Title = path;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
if (_directoryContents == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(newSearch))
|
||||
{
|
||||
if (_filteredContents != null)
|
||||
{
|
||||
_filteredContents = null;
|
||||
RaiseItemsChanged(-1);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Need to break this out the manual way so that the compiler can know
|
||||
// this is an ExploreListItem
|
||||
var filteredResults = ListHelpers.FilterList(
|
||||
_directoryContents,
|
||||
newSearch,
|
||||
(s, i) => ListHelpers.ScoreListItem(s, i));
|
||||
|
||||
if (_filteredContents != null)
|
||||
{
|
||||
lock (_filteredContents)
|
||||
{
|
||||
ListHelpers.InPlaceUpdateList<ExploreListItem>(_filteredContents, filteredResults);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_filteredContents = filteredResults.ToList();
|
||||
}
|
||||
|
||||
RaiseItemsChanged(-1);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
if (_filteredContents != null)
|
||||
{
|
||||
return _filteredContents.ToArray();
|
||||
}
|
||||
|
||||
if (_directoryContents != null)
|
||||
{
|
||||
return _directoryContents.ToArray();
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
if (!Path.Exists(_path))
|
||||
{
|
||||
EmptyContent = new CommandItem(title: Resources.Indexer_File_Does_Not_Exist);
|
||||
return [];
|
||||
}
|
||||
|
||||
var attr = File.GetAttributes(_path);
|
||||
|
||||
// detect whether its a directory or file
|
||||
if ((attr & FileAttributes.Directory) != FileAttributes.Directory)
|
||||
{
|
||||
EmptyContent = new CommandItem(title: Resources.Indexer_File_Is_File_Not_Folder);
|
||||
return [];
|
||||
}
|
||||
|
||||
var contents = Directory.EnumerateFileSystemEntries(_path);
|
||||
_directoryContents = contents
|
||||
.Select(s => new IndexerItem() { FullPath = s, FileName = Path.GetFileName(s) })
|
||||
.Select(i => new ExploreListItem(i))
|
||||
.ToList();
|
||||
|
||||
foreach (var i in _directoryContents)
|
||||
{
|
||||
i.PathChangeRequested += HandlePathChangeRequested;
|
||||
}
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
foreach (var item in _directoryContents)
|
||||
{
|
||||
IconInfo? icon = null;
|
||||
try
|
||||
{
|
||||
var stream = ThumbnailHelper.GetThumbnail(item.FilePath).Result;
|
||||
if (stream != null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
icon = new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
item.Icon = icon;
|
||||
}
|
||||
});
|
||||
|
||||
IsLoading = false;
|
||||
|
||||
return _directoryContents.ToArray();
|
||||
}
|
||||
|
||||
private void HandlePathChangeRequested(ExploreListItem sender, string path)
|
||||
{
|
||||
_directoryContents = null;
|
||||
_filteredContents = null;
|
||||
_path = path;
|
||||
Title = path;
|
||||
SearchText = string.Empty;
|
||||
RaiseItemsChanged(-1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Commands;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
|
||||
#nullable enable
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
/// <summary>
|
||||
/// This is almost more of just a sample than anything.
|
||||
/// </summary>
|
||||
internal sealed partial class ExploreListItem : ListItem
|
||||
{
|
||||
internal string FilePath { get; private set; }
|
||||
|
||||
internal event TypedEventHandler<ExploreListItem, string>? PathChangeRequested;
|
||||
|
||||
public ExploreListItem(IndexerItem indexerItem)
|
||||
: base(new NoOpCommand())
|
||||
{
|
||||
FilePath = indexerItem.FullPath;
|
||||
|
||||
Title = indexerItem.FileName;
|
||||
Subtitle = indexerItem.FullPath;
|
||||
List<CommandContextItem> context = [];
|
||||
if (indexerItem.IsDirectory())
|
||||
{
|
||||
Command = new AnonymousCommand(
|
||||
() => { PathChangeRequested?.Invoke(this, FilePath); })
|
||||
{
|
||||
Result = CommandResult.KeepOpen(),
|
||||
Name = Resources.Indexer_Command_Browse,
|
||||
};
|
||||
context.Add(new CommandContextItem(new DirectoryPage(indexerItem.FullPath)));
|
||||
}
|
||||
else
|
||||
{
|
||||
Command = new OpenFileCommand(indexerItem);
|
||||
}
|
||||
|
||||
MoreCommands = [
|
||||
..context,
|
||||
new CommandContextItem(new OpenWithCommand(indexerItem)),
|
||||
new CommandContextItem(new ShowFileInFolderCommand(indexerItem.FullPath) { Name = Resources.Indexer_Command_ShowInFolder }),
|
||||
new CommandContextItem(new CopyPathCommand(indexerItem)),
|
||||
new CommandContextItem(new OpenInConsoleCommand(indexerItem)),
|
||||
new CommandContextItem(new OpenPropertiesCommand(indexerItem)),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Data;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Indexer;
|
||||
using Microsoft.CmdPal.Ext.Indexer.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Storage.Streams;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
internal sealed partial class IndexerPage : DynamicListPage, IDisposable
|
||||
{
|
||||
private readonly List<IListItem> _indexerListItems = [];
|
||||
|
||||
private SearchQuery _searchQuery = new();
|
||||
|
||||
private uint _queryCookie = 10;
|
||||
|
||||
public IndexerPage()
|
||||
{
|
||||
Id = "com.microsoft.indexer.fileSearch";
|
||||
Icon = Icons.FileExplorer;
|
||||
Name = Resources.Indexer_Title;
|
||||
PlaceholderText = Resources.Indexer_PlaceholderText;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
if (oldSearch != newSearch)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
Query(newSearch);
|
||||
LoadMore();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => [.. _indexerListItems];
|
||||
|
||||
public override void LoadMore()
|
||||
{
|
||||
IsLoading = true;
|
||||
FetchItems(20);
|
||||
IsLoading = false;
|
||||
RaiseItemsChanged(_indexerListItems.Count);
|
||||
}
|
||||
|
||||
private void Query(string query)
|
||||
{
|
||||
++_queryCookie;
|
||||
_indexerListItems.Clear();
|
||||
_searchQuery.SearchResults.Clear();
|
||||
_searchQuery.CancelOutstandingQueries();
|
||||
|
||||
if (query == string.Empty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Stopwatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
_searchQuery.Execute(query, _queryCookie);
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.LogDebug($"Query time: {stopwatch.ElapsedMilliseconds} ms, query: \"{query}\"");
|
||||
}
|
||||
|
||||
private void FetchItems(int limit)
|
||||
{
|
||||
if (_searchQuery != null)
|
||||
{
|
||||
var cookie = _searchQuery.Cookie;
|
||||
if (cookie == _queryCookie)
|
||||
{
|
||||
var index = 0;
|
||||
SearchResult result;
|
||||
|
||||
var hasMoreItems = _searchQuery.FetchRows(_indexerListItems.Count, limit);
|
||||
|
||||
while (!_searchQuery.SearchResults.IsEmpty && _searchQuery.SearchResults.TryDequeue(out result) && ++index <= limit)
|
||||
{
|
||||
IconInfo icon = null;
|
||||
try
|
||||
{
|
||||
var stream = ThumbnailHelper.GetThumbnail(result.LaunchUri).Result;
|
||||
if (stream != null)
|
||||
{
|
||||
var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream));
|
||||
icon = new IconInfo(data, data);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to get the icon.", ex);
|
||||
}
|
||||
|
||||
_indexerListItems.Add(new IndexerListItem(new IndexerItem
|
||||
{
|
||||
FileName = result.ItemDisplayName,
|
||||
FullPath = result.LaunchUri,
|
||||
})
|
||||
{
|
||||
Icon = icon,
|
||||
});
|
||||
}
|
||||
|
||||
HasMoreItems = hasMoreItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_searchQuery = null;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,49 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 4.665C0 4.29773 0.297731 4 0.665 4H3.32533C3.6926 4 3.99033 4.29773 3.99033 4.665V7.99999H0V4.665Z" fill="url(#paint0_linear_1875_18082)"/>
|
||||
<path d="M5.11801 3.59869C4.86245 3.33492 4.8686 2.9134 5.13175 2.65721L7.04127 0.798149C7.30442 0.54195 7.72492 0.548088 7.98048 0.811859L9.83511 2.72606C10.0907 2.98983 10.0845 3.41135 9.82137 3.66755L7.91186 5.5266C7.64871 5.7828 7.22821 5.77667 6.97264 5.51289L5.11801 3.59869Z" fill="url(#paint1_linear_1875_18082)"/>
|
||||
<path d="M11.3248 6.96737C11.4366 6.61756 11.8108 6.42509 12.1604 6.53747L14.694 7.35177C15.0437 7.46415 15.2364 7.83883 15.1245 8.18864L14.3114 10.7309C14.1995 11.0807 13.8254 11.2731 13.4757 11.1608L10.9421 10.3465C10.5925 10.2341 10.3997 9.85939 10.5116 9.50958L11.3248 6.96737Z" fill="url(#paint2_linear_1875_18082)"/>
|
||||
<path d="M11.1783 1.8671C11.0653 1.51765 11.2568 1.14234 11.6061 1.02882L14.137 0.206196C14.4863 0.0926694 14.861 0.283918 14.9741 0.633361L15.7955 3.17288C15.9085 3.52233 15.717 3.89764 15.3677 4.01116L12.8368 4.83379C12.4875 4.94731 12.1128 4.75606 11.9997 4.40662L11.1783 1.8671Z" fill="url(#paint3_linear_1875_18082)"/>
|
||||
<path d="M0 8H3.99033V12H0V8Z" fill="url(#paint4_linear_1875_18082)"/>
|
||||
<path d="M3.99023 8H7.31556C7.68283 8 7.98056 8.29773 7.98056 8.665V12H3.99023V8Z" fill="url(#paint5_linear_1875_18082)"/>
|
||||
<path d="M0 12H3.99033V16H0.664999C0.29773 16 0 15.7023 0 15.335V12Z" fill="url(#paint6_linear_1875_18082)"/>
|
||||
<path d="M3.99023 12H7.98056V16H3.99023V12Z" fill="url(#paint7_linear_1875_18082)"/>
|
||||
<path d="M7.98047 12H11.3058C11.6731 12 11.9708 12.2977 11.9708 12.665V15.335C11.9708 15.7023 11.6731 16 11.3058 16H7.98047V12Z" fill="url(#paint8_linear_1875_18082)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_1875_18082" x1="-0.0906894" y1="3.72727" x2="4.54629" y2="8.62516" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#26AEE9"/>
|
||||
<stop offset="1" stop-color="#1EA1E4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1875_18082" x1="4.75107" y1="1.09701" x2="9.25021" y2="5.25282" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0339518" stop-color="#0B589A"/>
|
||||
<stop offset="1" stop-color="#14518A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint2_linear_1875_18082" x1="11.5241" y1="6.04652" x2="14.4446" y2="12.1313" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#24ACE8"/>
|
||||
<stop offset="1" stop-color="#1FA3E4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint3_linear_1875_18082" x1="10.8034" y1="1.00298" x2="15.638" y2="4.8226" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1694DA"/>
|
||||
<stop offset="1" stop-color="#0067B2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint4_linear_1875_18082" x1="-0.0906894" y1="7.72727" x2="4.54629" y2="12.6252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1997DB"/>
|
||||
<stop offset="1" stop-color="#027BD5"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint5_linear_1875_18082" x1="3.89954" y1="7.72727" x2="8.53652" y2="12.6252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1EA2E3"/>
|
||||
<stop offset="1" stop-color="#1790CB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint6_linear_1875_18082" x1="-0.0906894" y1="11.7273" x2="4.54629" y2="16.6252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#0B599C"/>
|
||||
<stop offset="1" stop-color="#175086"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint7_linear_1875_18082" x1="3.89954" y1="11.7273" x2="8.53652" y2="16.6252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#057FD5"/>
|
||||
<stop offset="1" stop-color="#0063AC"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint8_linear_1875_18082" x1="7.88978" y1="11.7273" x2="12.5268" y2="16.6252" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1892CF"/>
|
||||
<stop offset="1" stop-color="#0071C5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1,116 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
|
||||
/// <summary>
|
||||
/// A entry of the registry.
|
||||
/// </summary>
|
||||
internal sealed class RegistryEntry
|
||||
{
|
||||
#pragma warning disable CS8632
|
||||
/// <summary>
|
||||
/// Gets the full path to a registry key.
|
||||
/// </summary>
|
||||
internal string KeyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the registry key for this entry.
|
||||
/// </summary>
|
||||
internal RegistryKey? Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a possible exception that was occurred when try to open this registry key (e.g. <see cref="UnauthorizedAccessException"/>).
|
||||
/// </summary>
|
||||
internal Exception? Exception { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the current selected registry value.
|
||||
/// </summary>
|
||||
internal string? ValueName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the current selected registry value.
|
||||
/// </summary>
|
||||
internal object? ValueData { get; }
|
||||
|
||||
#pragma warning restore CS8632
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="keyPath">The full path to the registry key for this entry.</param>
|
||||
/// <param name="exception">A exception that was occurred when try to access this registry key.</param>
|
||||
internal RegistryEntry(string keyPath, Exception exception)
|
||||
{
|
||||
KeyPath = keyPath;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="RegistryKey"/> for this entry.</param>
|
||||
internal RegistryEntry(RegistryKey key)
|
||||
{
|
||||
KeyPath = key.Name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="RegistryEntry"/> class.
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="RegistryKey"/> for this entry.</param>
|
||||
/// <param name="valueName">The value name of the current selected registry value.</param>
|
||||
/// <param name="value">The value of the current selected registry value.</param>
|
||||
internal RegistryEntry(RegistryKey key, string valueName, object value)
|
||||
{
|
||||
KeyPath = key.Name;
|
||||
Key = key;
|
||||
ValueName = valueName;
|
||||
ValueData = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the registry key.
|
||||
/// </summary>
|
||||
/// <returns>A registry key.</returns>
|
||||
internal string GetRegistryKey()
|
||||
{
|
||||
return $"{Key?.Name ?? KeyPath}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the value name with the complete registry key.
|
||||
/// </summary>
|
||||
/// <returns>A value name with the complete registry key.</returns>
|
||||
internal string GetValueNameWithKey()
|
||||
{
|
||||
return $"{Key?.Name ?? KeyPath}\\\\{ValueName?.ToString() ?? string.Empty}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the value data of a value name inside a registry key.
|
||||
/// </summary>
|
||||
/// <returns>A value data.</returns>
|
||||
internal string GetValueData()
|
||||
{
|
||||
if (Key is null)
|
||||
{
|
||||
return KeyPath;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(ValueName))
|
||||
{
|
||||
return Key.Name;
|
||||
}
|
||||
|
||||
return ValueHelper.GetValue(Key, ValueName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Commands;
|
||||
|
||||
internal sealed partial class CopyRegistryInfoCommand : InvokableCommand
|
||||
{
|
||||
private readonly RegistryEntry _entry;
|
||||
private readonly string _stringToCopy;
|
||||
|
||||
internal CopyRegistryInfoCommand(RegistryEntry entry, CopyType typeToCopy)
|
||||
{
|
||||
if (typeToCopy == CopyType.Key)
|
||||
{
|
||||
Name = Resources.CopyKeyNamePath;
|
||||
Icon = new IconInfo("\xE8C8"); // Copy Icon
|
||||
_stringToCopy = entry.GetRegistryKey();
|
||||
}
|
||||
else if (typeToCopy == CopyType.ValueData)
|
||||
{
|
||||
Name = Resources.CopyValueData;
|
||||
Icon = new IconInfo("\xF413"); // CopyTo Icon
|
||||
_stringToCopy = entry.GetValueData();
|
||||
}
|
||||
else if (typeToCopy == CopyType.ValueName)
|
||||
{
|
||||
Name = Resources.CopyValueName;
|
||||
Icon = new IconInfo("\xE8C8"); // Copy Icon
|
||||
_stringToCopy = entry.GetValueNameWithKey();
|
||||
}
|
||||
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
ClipboardHelper.SetText(_stringToCopy);
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Resources;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.UI;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Commands;
|
||||
|
||||
internal sealed partial class OpenKeyInEditorCommand : InvokableCommand
|
||||
{
|
||||
private readonly RegistryEntry _entry;
|
||||
|
||||
internal OpenKeyInEditorCommand(RegistryEntry entry)
|
||||
{
|
||||
Name = Resources.OpenKeyInRegistryEditor;
|
||||
Icon = new IconInfo("\xE8A7"); // OpenInNewWindow icon
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
internal static bool TryToOpenInRegistryEditor(in RegistryEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
RegistryHelper.OpenRegistryKey(entry.Key?.Name ?? entry.KeyPath);
|
||||
return true;
|
||||
}
|
||||
catch (System.ComponentModel.Win32Exception)
|
||||
{
|
||||
// TODO GH #118 We need a convenient way to show errors to a user
|
||||
// MessageBox.Show(
|
||||
// Resources.OpenInRegistryEditorAccessExceptionText,
|
||||
// Resources.OpenInRegistryEditorAccessExceptionTitle,
|
||||
// MessageBoxButton.OK,
|
||||
// MessageBoxImage.Error);
|
||||
return false;
|
||||
}
|
||||
#pragma warning disable CS0168, IDE0059
|
||||
catch (Exception exception)
|
||||
{
|
||||
// TODO GH #108: Logging
|
||||
// Log.Exception("Error on opening Windows registry editor", exception, typeof(Main));
|
||||
return false;
|
||||
}
|
||||
#pragma warning restore CS0168, IDE0059
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
TryToOpenInRegistryEditor(_entry);
|
||||
|
||||
return CommandResult.KeepOpen();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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.CmdPal.Ext.Registry.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// This class contains names for important registry keys
|
||||
/// </summary>
|
||||
internal static class KeyName
|
||||
{
|
||||
/// <summary>
|
||||
/// The first name part of each base key without the underscore
|
||||
/// </summary>
|
||||
internal const string FirstPart = "HKEY";
|
||||
|
||||
/// <summary>
|
||||
/// The first name part of each base key follow by a underscore
|
||||
/// </summary>
|
||||
internal const string FirstPartUnderscore = "HKEY_";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_CLASSES_ROOT (see <see cref="Win32.Registry.ClassesRoot"/>)
|
||||
/// </summary>
|
||||
internal const string ClassRootShort = "HKCR";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_CURRENT_CONFIG (see <see cref="Win32.Registry.CurrentConfig"/>)
|
||||
/// </summary>
|
||||
internal const string CurrentConfigShort = "HKCC";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_CURRENT_USER (see <see cref="Win32.Registry.CurrentUser"/>)
|
||||
/// </summary>
|
||||
internal const string CurrentUserShort = "HKCU";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_LOCAL_MACHINE (see <see cref="Win32.Registry.LocalMachine"/>)
|
||||
/// </summary>
|
||||
internal const string LocalMachineShort = "HKLM";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_PERFORMANCE_DATA (see <see cref="Win32.Registry.PerformanceData"/>)
|
||||
/// </summary>
|
||||
internal const string PerformanceDataShort = "HKPD";
|
||||
|
||||
/// <summary>
|
||||
/// The short name for the base key HKEY_USERS (see <see cref="Win32.Registry.Users"/>)
|
||||
/// </summary>
|
||||
internal const string UsersShort = "HKU";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.CmdPal.Ext.Registry.Constants;
|
||||
|
||||
/// <summary>
|
||||
/// This class contain all maximum text length.
|
||||
/// </summary>
|
||||
public static class MaxTextLength
|
||||
{
|
||||
/// <summary>
|
||||
/// The maximum length for the title text length with two context menu symbols on the right.
|
||||
/// </summary>
|
||||
internal const int MaximumTitleLengthWithTwoSymbols = 44;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length for the title text length with three context menu symbols on the right.
|
||||
/// </summary>
|
||||
internal const int MaximumTitleLengthWithThreeSymbols = 40;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length for the sub-title text length with two context menu symbols on the right.
|
||||
/// </summary>
|
||||
internal const int MaximumSubTitleLengthWithTwoSymbols = 85;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum length for the sub-title text length with three context menu symbols on the right.
|
||||
/// </summary>
|
||||
internal const int MaximumSubTitleLengthWithThreeSymbols = 78;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.CmdPal.Ext.Registry;
|
||||
|
||||
public enum CopyType
|
||||
{
|
||||
Key,
|
||||
ValueData,
|
||||
ValueName,
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// 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.CmdPal.Ext.Registry.Enumerations;
|
||||
|
||||
/// <summary>
|
||||
/// The truncate side for a to long text
|
||||
/// </summary>
|
||||
internal enum TruncateSide
|
||||
{
|
||||
/// <summary>
|
||||
/// Truncate a text only from the right side
|
||||
/// </summary>
|
||||
OnlyFromLeft,
|
||||
|
||||
/// <summary>
|
||||
/// Truncate a text only from the left side
|
||||
/// </summary>
|
||||
OnlyFromRight,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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.Windows;
|
||||
using System.Windows.Input;
|
||||
using Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Commands;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to easier work with context menu entries
|
||||
/// </summary>
|
||||
internal static class ContextMenuHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Return a list with all context menu entries for the given <see cref="Result"/>
|
||||
/// <para>Symbols taken from <see href="https://learn.microsoft.com/windows/uwp/design/style/segoe-ui-symbol-font"/></para>
|
||||
/// </summary>
|
||||
internal static List<CommandContextItem> GetContextMenu(RegistryEntry entry)
|
||||
{
|
||||
var list = new List<CommandContextItem>();
|
||||
|
||||
if (string.IsNullOrEmpty(entry.ValueName))
|
||||
{
|
||||
list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.Key)));
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.ValueData)));
|
||||
list.Add(new CommandContextItem(new CopyRegistryInfoCommand(entry, CopyType.ValueName)));
|
||||
}
|
||||
|
||||
// list.Add(new CommandContextItem(new OpenKeyInEditorCommand(entry)));
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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.Text.RegularExpressions;
|
||||
|
||||
using Microsoft.CmdPal.Ext.Registry.Constants;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to easier work with queries
|
||||
/// </summary>
|
||||
internal static partial class QueryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// The character to distinguish if the search query contain multiple parts (typically "\\")
|
||||
/// </summary>
|
||||
internal const string QuerySplitCharacter = "\\\\";
|
||||
|
||||
/// <summary>
|
||||
/// A list that contain short names of all registry base keys
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<string, string> _shortBaseKeys = new Dictionary<string, string>(6)
|
||||
{
|
||||
{ Win32.Registry.ClassesRoot.Name, KeyName.ClassRootShort },
|
||||
{ Win32.Registry.CurrentConfig.Name, KeyName.CurrentConfigShort },
|
||||
{ Win32.Registry.CurrentUser.Name, KeyName.CurrentUserShort },
|
||||
{ Win32.Registry.LocalMachine.Name, KeyName.LocalMachineShort },
|
||||
{ Win32.Registry.PerformanceData.Name, KeyName.PerformanceDataShort },
|
||||
{ Win32.Registry.Users.Name, KeyName.UsersShort },
|
||||
};
|
||||
|
||||
[GeneratedRegex(@"/(?<=^(?:[^""]*""[^""]*"")*[^""]*)(?<!//.+)", RegexOptions.IgnoreCase, "en-US")]
|
||||
private static partial Regex FrontToBackSlashRegex();
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize the query to avoid issues with the regex
|
||||
/// </summary>
|
||||
/// <param name="query">Query containing front-slash</param>
|
||||
/// <returns>A string replacing all the front-slashes with back-slashes</returns>
|
||||
private static string SanitizeQuery(in string query)
|
||||
{
|
||||
var sanitizedQuery = FrontToBackSlashRegex().Replace(query, "\\");
|
||||
|
||||
return sanitizedQuery.Replace("\"", string.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the parts of a given query
|
||||
/// </summary>
|
||||
/// <param name="query">The query that could contain parts</param>
|
||||
/// <param name="queryKey">The key part of the query</param>
|
||||
/// <param name="queryValueName">The value name part of the query</param>
|
||||
/// <returns><see langword="true"/> when the query search for a key and a value name, otherwise <see langword="false"/></returns>
|
||||
internal static bool GetQueryParts(in string query, out string queryKey, out string queryValueName)
|
||||
{
|
||||
var sanitizedQuery = SanitizeQuery(query);
|
||||
|
||||
if (!sanitizedQuery.Contains(QuerySplitCharacter, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
queryKey = sanitizedQuery;
|
||||
queryValueName = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
var querySplit = sanitizedQuery.Split(QuerySplitCharacter);
|
||||
|
||||
queryKey = querySplit.First();
|
||||
queryValueName = querySplit.Last();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a registry key with a long base key
|
||||
/// </summary>
|
||||
/// <param name="registryKey">A registry key with a short base key</param>
|
||||
/// <returns>A registry key with a long base key</returns>
|
||||
internal static string GetKeyWithLongBaseKey(in string registryKey)
|
||||
{
|
||||
foreach (var shortName in _shortBaseKeys)
|
||||
{
|
||||
if (!registryKey.StartsWith(shortName.Value, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return registryKey.Replace(shortName.Value, shortName.Key, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return registryKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a registry key with a short base key (useful to reduce the text length of a registry key)
|
||||
/// </summary>
|
||||
/// <param name="registryKey">A registry key with a full base key</param>
|
||||
/// <returns>A registry key with a short base key</returns>
|
||||
internal static string GetKeyWithShortBaseKey(in string registryKey)
|
||||
{
|
||||
foreach (var shortName in _shortBaseKeys)
|
||||
{
|
||||
if (!registryKey.StartsWith(shortName.Key, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return registryKey.Replace(shortName.Key, shortName.Value, StringComparison.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
return registryKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
using Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Constants;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to easier work with the registry
|
||||
/// </summary>
|
||||
internal static class RegistryHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// A list that contain all registry base keys in a long/full version and in a short version (e.g HKLM = HKEY_LOCAL_MACHINE)
|
||||
/// </summary>
|
||||
private static readonly IReadOnlyDictionary<string, RegistryKey> _baseKeys = new Dictionary<string, RegistryKey>(12)
|
||||
{
|
||||
{ KeyName.ClassRootShort, Win32.Registry.ClassesRoot },
|
||||
{ Win32.Registry.ClassesRoot.Name, Win32.Registry.ClassesRoot },
|
||||
{ KeyName.CurrentConfigShort, Win32.Registry.CurrentConfig },
|
||||
{ Win32.Registry.CurrentConfig.Name, Win32.Registry.CurrentConfig },
|
||||
{ KeyName.CurrentUserShort, Win32.Registry.CurrentUser },
|
||||
{ Win32.Registry.CurrentUser.Name, Win32.Registry.CurrentUser },
|
||||
{ KeyName.LocalMachineShort, Win32.Registry.LocalMachine },
|
||||
{ Win32.Registry.LocalMachine.Name, Win32.Registry.LocalMachine },
|
||||
{ KeyName.PerformanceDataShort, Win32.Registry.PerformanceData },
|
||||
{ Win32.Registry.PerformanceData.Name, Win32.Registry.PerformanceData },
|
||||
{ KeyName.UsersShort, Win32.Registry.Users },
|
||||
{ Win32.Registry.Users.Name, Win32.Registry.Users },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Try to find registry base keys based on the given query
|
||||
/// </summary>
|
||||
/// <param name="query">The query to search</param>
|
||||
/// <returns>A combination of a list of base <see cref="RegistryKey"/> and the sub keys</returns>
|
||||
#pragma warning disable CS8632
|
||||
internal static (IEnumerable<RegistryKey>? BaseKey, string SubKey) GetRegistryBaseKey(in string query)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(query))
|
||||
{
|
||||
return (null, string.Empty);
|
||||
}
|
||||
|
||||
var baseKey = query.Split('\\').FirstOrDefault() ?? string.Empty;
|
||||
var subKey = query.Replace(baseKey, string.Empty, StringComparison.InvariantCultureIgnoreCase).TrimStart('\\');
|
||||
|
||||
var baseKeyResult = _baseKeys
|
||||
.Where(found => found.Key.StartsWith(baseKey, StringComparison.InvariantCultureIgnoreCase))
|
||||
.Select(found => found.Value)
|
||||
.Distinct();
|
||||
|
||||
return (baseKeyResult, subKey);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a list of all registry base key
|
||||
/// </summary>
|
||||
/// <returns>A list with all registry base keys</returns>
|
||||
internal static ICollection<RegistryEntry> GetAllBaseKeys()
|
||||
{
|
||||
return new Collection<RegistryEntry>
|
||||
{
|
||||
new(Win32.Registry.ClassesRoot),
|
||||
new(Win32.Registry.CurrentConfig),
|
||||
new(Win32.Registry.CurrentUser),
|
||||
new(Win32.Registry.LocalMachine),
|
||||
new(Win32.Registry.PerformanceData),
|
||||
new(Win32.Registry.Users),
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for the given sub-key path in the given registry base key
|
||||
/// </summary>
|
||||
/// <param name="baseKey">The base <see cref="RegistryKey"/></param>
|
||||
/// <param name="subKeyPath">The path of the registry sub-key</param>
|
||||
/// <returns>A list with all found registry keys</returns>
|
||||
internal static ICollection<RegistryEntry> SearchForSubKey(in RegistryKey baseKey, in string subKeyPath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(subKeyPath))
|
||||
{
|
||||
return FindSubKey(baseKey, string.Empty);
|
||||
}
|
||||
|
||||
var subKeysNames = subKeyPath.Split('\\');
|
||||
var index = 0;
|
||||
RegistryKey? subKey = baseKey;
|
||||
#pragma warning restore CS8632
|
||||
ICollection<RegistryEntry> result;
|
||||
|
||||
do
|
||||
{
|
||||
result = FindSubKey(subKey, subKeysNames.ElementAtOrDefault(index) ?? string.Empty);
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
// If a subKey can't be found, show no results.
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.Count == 1 && index < subKeysNames.Length)
|
||||
{
|
||||
subKey = result.First().Key;
|
||||
}
|
||||
|
||||
if (result.Count > 1 || subKey == null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
while (index < subKeysNames.Length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a human readable summary of a given <see cref="RegistryKey"/>
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="RegistryKey"/> for the summary</param>
|
||||
/// <returns>A human readable summary</returns>
|
||||
internal static string GetSummary(in RegistryKey key)
|
||||
{
|
||||
return $"{Resources.SubKeys} {key.SubKeyCount} - {Resources.Values} {key.ValueCount}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Open a given registry key in the registry editor
|
||||
/// </summary>
|
||||
/// <param name="fullKey">The registry key to open</param>
|
||||
internal static void OpenRegistryKey(in string fullKey)
|
||||
{
|
||||
// Set the registry key
|
||||
Win32.Registry.SetValue(@"HKEY_Current_User\Software\Microsoft\Windows\CurrentVersion\Applets\Regedit", "LastKey", fullKey);
|
||||
|
||||
// Open regedit.exe with multi-instance option
|
||||
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "regedit.exe",
|
||||
Arguments = "-m",
|
||||
UseShellExecute = true,
|
||||
Verb = "runas", // Runs as Administrator
|
||||
};
|
||||
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to find the given registry sub-key in the given registry parent-key
|
||||
/// </summary>
|
||||
/// <param name="parentKey">The parent-key, also the root to start the search</param>
|
||||
/// <param name="searchSubKey">The sub-key to find</param>
|
||||
/// <returns>A list with all found registry sub-keys</returns>
|
||||
private static Collection<RegistryEntry> FindSubKey(in RegistryKey parentKey, in string searchSubKey)
|
||||
{
|
||||
var list = new Collection<RegistryEntry>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var subKey in parentKey.GetSubKeyNames().OrderBy(found => found))
|
||||
{
|
||||
if (!subKey.StartsWith(searchSubKey, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.Equals(subKey, searchSubKey, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree);
|
||||
if (key != null)
|
||||
{
|
||||
list.Add(new RegistryEntry(key));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var key = parentKey.OpenSubKey(subKey, RegistryKeyPermissionCheck.ReadSubTree);
|
||||
if (key != null)
|
||||
{
|
||||
list.Add(new RegistryEntry(key));
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
list.Add(new RegistryEntry($"{parentKey.Name}\\{subKey}", exception));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
list.Add(new RegistryEntry(parentKey.Name, ex));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a list with a registry sub-keys of the given registry parent-key
|
||||
/// </summary>
|
||||
/// <param name="parentKey">The registry parent-key</param>
|
||||
/// <param name="maxCount">(optional) The maximum count of the results</param>
|
||||
/// <returns>A list with all found registry sub-keys</returns>
|
||||
private static Collection<RegistryEntry> GetAllSubKeys(in RegistryKey parentKey, in int maxCount = 50)
|
||||
{
|
||||
var list = new Collection<RegistryEntry>();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var subKey in parentKey.GetSubKeyNames())
|
||||
{
|
||||
if (list.Count >= maxCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
list.Add(new RegistryEntry(parentKey));
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
list.Add(new RegistryEntry(parentKey.Name, exception));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Commands;
|
||||
using Microsoft.CmdPal.Ext.Registry.Constants;
|
||||
using Microsoft.CmdPal.Ext.Registry.Enumerations;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to easier work with results
|
||||
/// </summary>
|
||||
internal static class ResultHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Return a list with <see cref="Result"/>s, based on the given list
|
||||
/// </summary>
|
||||
/// <param name="list">The original result list to convert</param>
|
||||
/// <returns>A list with <see cref="Result"/></returns>
|
||||
internal static List<ListItem> GetResultList(in IEnumerable<RegistryEntry> list)
|
||||
{
|
||||
var resultList = new List<ListItem>();
|
||||
|
||||
foreach (var entry in list)
|
||||
{
|
||||
var result = new ListItem(new OpenKeyInEditorCommand(entry))
|
||||
{
|
||||
Icon = RegistryListPage.RegistryIcon,
|
||||
MoreCommands = ContextMenuHelper.GetContextMenu(entry).ToArray(),
|
||||
};
|
||||
|
||||
if (entry.Exception is null && entry.Key is not null)
|
||||
{
|
||||
// when key contains keys or fields
|
||||
result.TextToSuggest = entry.Key.Name;
|
||||
result.Subtitle = RegistryHelper.GetSummary(entry.Key);
|
||||
result.Title = GetTruncatedText(entry.Key.Name, MaxTextLength.MaximumTitleLengthWithTwoSymbols);
|
||||
}
|
||||
else if (entry.Key is null && entry.Exception is not null)
|
||||
{
|
||||
// on error (e.g access denied)
|
||||
result.TextToSuggest = entry.KeyPath;
|
||||
result.Subtitle = GetTruncatedText(entry.Exception.Message, MaxTextLength.MaximumSubTitleLengthWithTwoSymbols, TruncateSide.OnlyFromRight);
|
||||
result.Title = GetTruncatedText(entry.KeyPath, MaxTextLength.MaximumTitleLengthWithTwoSymbols);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.TextToSuggest = entry.KeyPath;
|
||||
result.Title = GetTruncatedText(entry.KeyPath, MaxTextLength.MaximumTitleLengthWithTwoSymbols);
|
||||
}
|
||||
|
||||
// result.ContextData = entry;
|
||||
// TODO GH #126 Investigate tool tips, result.ToolTipData = new ToolTipData(Resources.RegistryKey, $"{Resources.KeyName} {result.Title}");
|
||||
resultList.Add(result);
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
#pragma warning disable CS8632
|
||||
internal static List<ListItem> GetValuesFromKey(in RegistryKey? key, string searchValue = "")
|
||||
{
|
||||
#pragma warning restore CS8632
|
||||
if (key is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var valueList = new List<KeyValuePair<string, object>>(key.ValueCount);
|
||||
|
||||
var resultList = new List<ListItem>();
|
||||
|
||||
try
|
||||
{
|
||||
var valueNames = key.GetValueNames();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var valueName in valueNames)
|
||||
{
|
||||
var value = key.GetValue(valueName);
|
||||
if (value != null)
|
||||
{
|
||||
valueList.Add(KeyValuePair.Create(valueName, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception valueException)
|
||||
{
|
||||
var registryEntry = new RegistryEntry(key.Name, valueException);
|
||||
|
||||
resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry))
|
||||
{
|
||||
Icon = RegistryListPage.RegistryIcon,
|
||||
Subtitle = GetTruncatedText(valueException.Message, MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight),
|
||||
Title = GetTruncatedText(key.Name, MaxTextLength.MaximumTitleLengthWithThreeSymbols),
|
||||
MoreCommands = ContextMenuHelper.GetContextMenu(registryEntry).ToArray(),
|
||||
|
||||
// TODO --> Investigate ToolTipData = new ToolTipData(valueException.Message, valueException.ToString()),
|
||||
});
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(searchValue))
|
||||
{
|
||||
var filteredValueName = valueList.Where(found => found.Key.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase));
|
||||
var filteredValueList = valueList.Where(found => found.Value.ToString()?.Contains(searchValue, StringComparison.InvariantCultureIgnoreCase) ?? false);
|
||||
|
||||
valueList = filteredValueName.Concat(filteredValueList).Distinct().ToList();
|
||||
}
|
||||
|
||||
foreach (var valueEntry in valueList.OrderBy(found => found.Key))
|
||||
{
|
||||
var valueName = valueEntry.Key;
|
||||
if (string.IsNullOrEmpty(valueName))
|
||||
{
|
||||
valueName = "(Default)";
|
||||
}
|
||||
|
||||
var registryEntry = new RegistryEntry(key, valueEntry.Key, valueEntry.Value);
|
||||
|
||||
resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry))
|
||||
{
|
||||
Icon = RegistryListPage.RegistryIcon,
|
||||
Subtitle = GetTruncatedText(GetSubTileForRegistryValue(key, valueEntry), MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight),
|
||||
Title = GetTruncatedText(valueName, MaxTextLength.MaximumTitleLengthWithThreeSymbols),
|
||||
MoreCommands = ContextMenuHelper.GetContextMenu(registryEntry).ToArray(),
|
||||
|
||||
// TODO Investigate -->ToolTipData = new ToolTipData(Resources.RegistryValue, GetToolTipTextForRegistryValue(key, valueEntry)),
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
var registryEntry = new RegistryEntry(key.Name, exception);
|
||||
|
||||
resultList.Add(new ListItem(new OpenKeyInEditorCommand(registryEntry))
|
||||
{
|
||||
Icon = RegistryListPage.RegistryIcon,
|
||||
Subtitle = GetTruncatedText(exception.Message, MaxTextLength.MaximumSubTitleLengthWithThreeSymbols, TruncateSide.OnlyFromRight),
|
||||
Title = GetTruncatedText(key.Name, MaxTextLength.MaximumTitleLengthWithThreeSymbols),
|
||||
});
|
||||
}
|
||||
|
||||
return resultList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a truncated name
|
||||
/// </summary>
|
||||
/// <param name="text">The text to truncate</param>
|
||||
/// <param name="maxLength">The maximum length of the text</param>
|
||||
/// <param name="truncateSide">(optional) The side of the truncate</param>
|
||||
/// <returns>A truncated text with a maximum length</returns>
|
||||
internal static string GetTruncatedText(string text, in int maxLength, TruncateSide truncateSide = TruncateSide.OnlyFromLeft)
|
||||
{
|
||||
if (truncateSide == TruncateSide.OnlyFromLeft)
|
||||
{
|
||||
if (text.Length > maxLength)
|
||||
{
|
||||
text = QueryHelper.GetKeyWithShortBaseKey(text);
|
||||
}
|
||||
|
||||
return text.Length > maxLength ? $"...{text[^maxLength..]}" : text;
|
||||
}
|
||||
else
|
||||
{
|
||||
return text.Length > maxLength ? $"{text[0..maxLength]}..." : text;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the tool-tip text for a registry value
|
||||
/// </summary>
|
||||
/// <param name="key">The registry key for the tool-tip</param>
|
||||
/// <param name="valueEntry">The value name and value of the registry value</param>
|
||||
/// <returns>A tool-tip text</returns>
|
||||
private static string GetToolTipTextForRegistryValue(RegistryKey key, KeyValuePair<string, object> valueEntry)
|
||||
{
|
||||
return $"{Resources.KeyName} {key.Name}{Environment.NewLine}"
|
||||
+ $"{Resources.Name} {valueEntry.Key}{Environment.NewLine}"
|
||||
+ $"{Resources.Type} {ValueHelper.GetType(key, valueEntry.Key)}{Environment.NewLine}"
|
||||
+ $"{Resources.Value} {ValueHelper.GetValue(key, valueEntry.Key)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the sub-title text for a registry value
|
||||
/// </summary>
|
||||
/// <param name="key">The registry key for the sub-title</param>
|
||||
/// <param name="valueEntry">The value name and value of the registry value</param>
|
||||
/// <returns>A sub-title text</returns>
|
||||
private static string GetSubTileForRegistryValue(RegistryKey key, KeyValuePair<string, object> valueEntry)
|
||||
{
|
||||
return $"{Resources.Type} {ValueHelper.GetType(key, valueEntry.Key)}"
|
||||
+ $" - {Resources.Value} {ValueHelper.GetValue(key, valueEntry.Key, 50)}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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.Linq;
|
||||
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class to easier work with values of a <see cref="RegistryKey"/>
|
||||
/// </summary>
|
||||
internal static class ValueHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Return a human readable value data, of the given value name inside the given <see cref="RegistryKey"/>
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="RegistryKey"/> that should contain the value name.</param>
|
||||
/// <param name="valueName">The name of the value.</param>
|
||||
/// <param name="maxLength">The maximum length for the human readable value.</param>
|
||||
/// <returns>A human readable value data.</returns>
|
||||
internal static string GetValue(in RegistryKey key, in string valueName, int maxLength = int.MaxValue)
|
||||
{
|
||||
var unformattedValue = key.GetValue(valueName);
|
||||
|
||||
if (unformattedValue == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Cannot proceed when {nameof(unformattedValue)} is null.");
|
||||
}
|
||||
|
||||
var valueData = key.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.DWord => $"0x{unformattedValue:X8} ({(uint)(int)unformattedValue})",
|
||||
RegistryValueKind.QWord => $"0x{unformattedValue:X16} ({(ulong)(long)unformattedValue})",
|
||||
#pragma warning disable CS8604 // Possible null reference argument.
|
||||
RegistryValueKind.Binary => (unformattedValue as byte[]).Aggregate(string.Empty, (current, singleByte) => $"{current} {singleByte:X2}"),
|
||||
#pragma warning restore CS8604 // Possible null reference argument.
|
||||
_ => $"{unformattedValue}",
|
||||
};
|
||||
|
||||
return valueData.Length > maxLength
|
||||
? $"{valueData.Substring(0, maxLength)}..."
|
||||
: valueData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the registry type name of a given value name inside a given <see cref="RegistryKey"/>
|
||||
/// </summary>
|
||||
/// <param name="key">The <see cref="RegistryKey"/> that should contain the value name</param>
|
||||
/// <param name="valueName">The name of the value</param>
|
||||
/// <returns>A registry type name</returns>
|
||||
internal static object GetType(RegistryKey key, string valueName)
|
||||
{
|
||||
return key.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.None => Resources.RegistryValueKindNone,
|
||||
RegistryValueKind.Unknown => Resources.RegistryValueKindUnknown,
|
||||
RegistryValueKind.String => "REG_SZ",
|
||||
RegistryValueKind.ExpandString => "REG_EXPAND_SZ",
|
||||
RegistryValueKind.MultiString => "REG_MULTI_SZ",
|
||||
RegistryValueKind.Binary => "REG_BINARY",
|
||||
RegistryValueKind.DWord => "REG_DWORD",
|
||||
RegistryValueKind.QWord => "REG_QWORD",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(valueName)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Registry</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>Microsoft.CmdPal.Ext.Registry.pri</ProjectPriFileName>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.ServiceProcess.ServiceController" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\Registry.svg" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\Registry.png">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Update="Assets\Registry.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>ResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Registry.Classes;
|
||||
using Microsoft.CmdPal.Ext.Registry.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Registry.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry;
|
||||
|
||||
internal sealed partial class RegistryListPage : DynamicListPage
|
||||
{
|
||||
public static IconInfo RegistryIcon { get; } = new("\uE74C"); // OEM
|
||||
|
||||
private readonly CommandItem _emptyMessage;
|
||||
|
||||
public RegistryListPage()
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg");
|
||||
Name = Title = Resources.Registry_Page_Title;
|
||||
Id = "com.microsoft.cmdpal.registry";
|
||||
_emptyMessage = new CommandItem()
|
||||
{
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg"),
|
||||
Title = Resources.Registry_Key_Not_Found,
|
||||
Subtitle = SearchText,
|
||||
};
|
||||
EmptyContent = _emptyMessage;
|
||||
}
|
||||
|
||||
public List<ListItem> Query(string query)
|
||||
{
|
||||
if (query is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var searchForValueName = QueryHelper.GetQueryParts(query, out var queryKey, out var queryValueName);
|
||||
|
||||
var (baseKeyList, subKey) = RegistryHelper.GetRegistryBaseKey(queryKey);
|
||||
if (baseKeyList is null)
|
||||
{
|
||||
// no base key found
|
||||
return ResultHelper.GetResultList(RegistryHelper.GetAllBaseKeys());
|
||||
}
|
||||
else if (baseKeyList.Count() == 1)
|
||||
{
|
||||
// only one base key was found -> start search for the sub-key
|
||||
var list = RegistryHelper.SearchForSubKey(baseKeyList.First(), subKey);
|
||||
|
||||
// when only one sub-key was found and a user search for values ("\\")
|
||||
// show the filtered list of values of one sub-key
|
||||
return searchForValueName && list.Count == 1
|
||||
? ResultHelper.GetValuesFromKey(list.First().Key, queryValueName)
|
||||
: ResultHelper.GetResultList(list);
|
||||
}
|
||||
else if (baseKeyList.Count() > 1)
|
||||
{
|
||||
// more than one base key was found -> show results
|
||||
return ResultHelper.GetResultList(baseKeyList.Select(found => new RegistryEntry(found)));
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
_emptyMessage.Subtitle = newSearch;
|
||||
RaiseItemsChanged(0);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => Query(SearchText).ToArray();
|
||||
}
|
||||
243
src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Registry/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,243 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.Registry.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy key name (path).
|
||||
/// </summary>
|
||||
internal static string CopyKeyNamePath {
|
||||
get {
|
||||
return ResourceManager.GetString("CopyKeyNamePath", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy value data.
|
||||
/// </summary>
|
||||
internal static string CopyValueData {
|
||||
get {
|
||||
return ResourceManager.GetString("CopyValueData", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy value name.
|
||||
/// </summary>
|
||||
internal static string CopyValueName {
|
||||
get {
|
||||
return ResourceManager.GetString("CopyValueName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Key name:.
|
||||
/// </summary>
|
||||
internal static string KeyName {
|
||||
get {
|
||||
return ResourceManager.GetString("KeyName", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Name:.
|
||||
/// </summary>
|
||||
internal static string Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to You do not have enough rights to open the Windows registry editor.
|
||||
/// </summary>
|
||||
internal static string OpenInRegistryEditorAccessExceptionText {
|
||||
get {
|
||||
return ResourceManager.GetString("OpenInRegistryEditorAccessExceptionText", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error on open registry editor.
|
||||
/// </summary>
|
||||
internal static string OpenInRegistryEditorAccessExceptionTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("OpenInRegistryEditorAccessExceptionTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Open key in registry editor.
|
||||
/// </summary>
|
||||
internal static string OpenKeyInRegistryEditor {
|
||||
get {
|
||||
return ResourceManager.GetString("OpenKeyInRegistryEditor", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Navigates inside the Windows Registry.
|
||||
/// </summary>
|
||||
internal static string PluginDescription {
|
||||
get {
|
||||
return ResourceManager.GetString("PluginDescription", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Registry Plugin.
|
||||
/// </summary>
|
||||
internal static string PluginTitle {
|
||||
get {
|
||||
return ResourceManager.GetString("PluginTitle", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Registry key not found.
|
||||
/// </summary>
|
||||
internal static string Registry_Key_Not_Found {
|
||||
get {
|
||||
return ResourceManager.GetString("Registry_Key_Not_Found", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Windows Registry.
|
||||
/// </summary>
|
||||
internal static string Registry_Page_Title {
|
||||
get {
|
||||
return ResourceManager.GetString("Registry_Page_Title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Registry key.
|
||||
/// </summary>
|
||||
internal static string RegistryKey {
|
||||
get {
|
||||
return ResourceManager.GetString("RegistryKey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Registry value.
|
||||
/// </summary>
|
||||
internal static string RegistryValue {
|
||||
get {
|
||||
return ResourceManager.GetString("RegistryValue", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to No data type.
|
||||
/// </summary>
|
||||
internal static string RegistryValueKindNone {
|
||||
get {
|
||||
return ResourceManager.GetString("RegistryValueKindNone", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Unsupported data type.
|
||||
/// </summary>
|
||||
internal static string RegistryValueKindUnknown {
|
||||
get {
|
||||
return ResourceManager.GetString("RegistryValueKindUnknown", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Subkeys:.
|
||||
/// </summary>
|
||||
internal static string SubKeys {
|
||||
get {
|
||||
return ResourceManager.GetString("SubKeys", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Type:.
|
||||
/// </summary>
|
||||
internal static string Type {
|
||||
get {
|
||||
return ResourceManager.GetString("Type", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Value:.
|
||||
/// </summary>
|
||||
internal static string Value {
|
||||
get {
|
||||
return ResourceManager.GetString("Value", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Values:.
|
||||
/// </summary>
|
||||
internal static string Values {
|
||||
get {
|
||||
return ResourceManager.GetString("Values", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="PluginTitle" xml:space="preserve">
|
||||
<value>Registry Plugin</value>
|
||||
</data>
|
||||
<data name="PluginDescription" xml:space="preserve">
|
||||
<value>Navigates inside the Windows Registry</value>
|
||||
<comment>"this built into Windows the OS. translate accordingly, https://learn.microsoft.com/troubleshoot/windows-server/performance/windows-registry-advanced-users is an example of it translated in German"</comment>
|
||||
</data>
|
||||
<data name="CopyKeyNamePath" xml:space="preserve">
|
||||
<value>Copy key name (path)</value>
|
||||
<comment>'The maximum size of a key name is 255 characters.'</comment>
|
||||
</data>
|
||||
<data name="KeyName" xml:space="preserve">
|
||||
<value>Key name:</value>
|
||||
<comment>'The maximum size of a key name is 255 characters.'</comment>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>Name:</value>
|
||||
</data>
|
||||
<data name="CopyValueName" xml:space="preserve">
|
||||
<value>Copy value name</value>
|
||||
</data>
|
||||
<data name="OpenKeyInRegistryEditor" xml:space="preserve">
|
||||
<value>Open key in registry editor</value>
|
||||
<comment>"registry editor" is the name of the OS built-in application</comment>
|
||||
</data>
|
||||
<data name="OpenInRegistryEditorAccessExceptionText" xml:space="preserve">
|
||||
<value>You do not have enough rights to open the Windows registry editor</value>
|
||||
<comment>"registry editor" is the name of the OS built-in application </comment>
|
||||
</data>
|
||||
<data name="OpenInRegistryEditorAccessExceptionTitle" xml:space="preserve">
|
||||
<value>Error on open registry editor</value>
|
||||
<comment>"registry editor" is the name of the OS built-in application</comment>
|
||||
</data>
|
||||
<data name="SubKeys" xml:space="preserve">
|
||||
<value>Subkeys:</value>
|
||||
</data>
|
||||
<data name="Values" xml:space="preserve">
|
||||
<value>Values:</value>
|
||||
</data>
|
||||
<data name="RegistryKey" xml:space="preserve">
|
||||
<value>Registry key</value>
|
||||
</data>
|
||||
<data name="RegistryValue" xml:space="preserve">
|
||||
<value>Registry value</value>
|
||||
</data>
|
||||
<data name="Type" xml:space="preserve">
|
||||
<value>Type:</value>
|
||||
<comment>See https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types for proper context of how to translate 'type'</comment>
|
||||
</data>
|
||||
<data name="Value" xml:space="preserve">
|
||||
<value>Value:</value>
|
||||
<comment>See https://learn.microsoft.com/windows/win32/sysinfo/registry-value-types for proper context of how to translate 'value'</comment>
|
||||
</data>
|
||||
<data name="RegistryValueKindNone" xml:space="preserve">
|
||||
<value>No data type</value>
|
||||
</data>
|
||||
<data name="RegistryValueKindUnknown" xml:space="preserve">
|
||||
<value>Unsupported data type</value>
|
||||
</data>
|
||||
<data name="CopyValueData" xml:space="preserve">
|
||||
<value>Copy value data</value>
|
||||
</data>
|
||||
<data name="Registry_Page_Title" xml:space="preserve">
|
||||
<value>Windows Registry</value>
|
||||
</data>
|
||||
<data name="Registry_Key_Not_Found" xml:space="preserve">
|
||||
<value>Registry key not found</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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 Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Registry;
|
||||
|
||||
public partial class RegistryCommandsProvider : CommandProvider
|
||||
{
|
||||
public RegistryCommandsProvider()
|
||||
{
|
||||
Id = "Windows.Registry";
|
||||
DisplayName = $"Windows Registry";
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\Registry.svg");
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
return [
|
||||
new CommandItem(new RegistryListPage())
|
||||
{
|
||||
Title = "Registry",
|
||||
Subtitle = "Navigate the Windows registry",
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Shell.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Commands;
|
||||
|
||||
internal sealed partial class ExecuteItem : InvokableCommand
|
||||
{
|
||||
private readonly SettingsManager _settings;
|
||||
private readonly RunAsType _runas;
|
||||
|
||||
public string Cmd { get; internal set; } = string.Empty;
|
||||
|
||||
private static readonly char[] Separator = [' '];
|
||||
|
||||
public ExecuteItem(string cmd, SettingsManager settings, RunAsType type = RunAsType.None)
|
||||
{
|
||||
if (type == RunAsType.Administrator)
|
||||
{
|
||||
Name = Properties.Resources.cmd_run_as_administrator;
|
||||
Icon = new IconInfo("\xE7EF"); // Admin Icon
|
||||
}
|
||||
else if (type == RunAsType.OtherUser)
|
||||
{
|
||||
Name = Properties.Resources.cmd_run_as_user;
|
||||
Icon = new IconInfo("\xE7EE"); // User Icon
|
||||
}
|
||||
else
|
||||
{
|
||||
Name = Properties.Resources.generic_run_command;
|
||||
Icon = new IconInfo("\uE751"); // Return Key Icon
|
||||
}
|
||||
|
||||
Cmd = cmd;
|
||||
_settings = settings;
|
||||
_runas = type;
|
||||
}
|
||||
|
||||
private static bool ExistInPath(string filename)
|
||||
{
|
||||
if (File.Exists(filename))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var values = Environment.GetEnvironmentVariable("PATH");
|
||||
if (values != null)
|
||||
{
|
||||
foreach (var path in values.Split(';'))
|
||||
{
|
||||
var path1 = Path.Combine(path, filename);
|
||||
var path2 = Path.Combine(path, filename + ".exe");
|
||||
if (File.Exists(path1) || File.Exists(path2))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Execute(Func<ProcessStartInfo, Process?> startProcess, ProcessStartInfo info)
|
||||
{
|
||||
if (startProcess == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
startProcess(info);
|
||||
}
|
||||
catch (FileNotFoundException e)
|
||||
{
|
||||
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
|
||||
var message = $"{Properties.Resources.cmd_command_not_found}: {e.Message}";
|
||||
|
||||
// GH TODO #138 -- show this message once that's wired up
|
||||
// _context.API.ShowMsg(name, message);
|
||||
}
|
||||
catch (Win32Exception e)
|
||||
{
|
||||
var name = "Plugin: " + Properties.Resources.cmd_plugin_name;
|
||||
var message = $"{Properties.Resources.cmd_command_failed}: {e.Message}";
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = name + message });
|
||||
|
||||
// GH TODO #138 -- show this message once that's wired up
|
||||
// _context.API.ShowMsg(name, message);
|
||||
}
|
||||
}
|
||||
|
||||
public static ProcessStartInfo SetProcessStartInfo(string fileName, string workingDirectory = "", string arguments = "", string verb = "")
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
WorkingDirectory = workingDirectory,
|
||||
Arguments = arguments,
|
||||
Verb = verb,
|
||||
};
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
private ProcessStartInfo PrepareProcessStartInfo(string command, RunAsType runAs = RunAsType.None)
|
||||
{
|
||||
command = Environment.ExpandEnvironmentVariables(command);
|
||||
var workingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
// Set runAsArg
|
||||
var runAsVerbArg = string.Empty;
|
||||
if (runAs == RunAsType.OtherUser)
|
||||
{
|
||||
runAsVerbArg = "runAsUser";
|
||||
}
|
||||
else if (runAs == RunAsType.Administrator || _settings.RunAsAdministrator)
|
||||
{
|
||||
runAsVerbArg = "runAs";
|
||||
}
|
||||
|
||||
if (Enum.TryParse<ExecutionShell>(_settings.ShellCommandExecution, out var executionShell))
|
||||
{
|
||||
ProcessStartInfo info;
|
||||
if (executionShell == ExecutionShell.Cmd)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen ? $"/k \"{command}\"" : $"/c \"{command}\" & pause";
|
||||
|
||||
info = SetProcessStartInfo("cmd.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.Powershell)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen
|
||||
? $"-NoExit \"{command}\""
|
||||
: $"\"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
|
||||
info = SetProcessStartInfo("powershell.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.PowerShellSeven)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen
|
||||
? $"-NoExit -C \"{command}\""
|
||||
: $"-C \"{command} ; Read-Host -Prompt \\\"{Resources.run_plugin_cmd_wait_message}\\\"\"";
|
||||
info = SetProcessStartInfo("pwsh.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.WindowsTerminalCmd)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen ? $"cmd.exe /k \"{command}\"" : $"cmd.exe /c \"{command}\" & pause";
|
||||
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.WindowsTerminalPowerShell)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen ? $"powershell -NoExit -C \"{command}\"" : $"powershell -C \"{command}\"";
|
||||
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.WindowsTerminalPowerShellSeven)
|
||||
{
|
||||
var arguments = _settings.LeaveShellOpen ? $"pwsh.exe -NoExit -C \"{command}\"" : $"pwsh.exe -C \"{command}\"";
|
||||
info = SetProcessStartInfo("wt.exe", workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
else if (executionShell == ExecutionShell.RunCommand)
|
||||
{
|
||||
// Open explorer if the path is a file or directory
|
||||
if (Directory.Exists(command) || File.Exists(command))
|
||||
{
|
||||
info = SetProcessStartInfo("explorer.exe", arguments: command, verb: runAsVerbArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parts = command.Split(Separator, 2);
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
var filename = parts[0];
|
||||
if (ExistInPath(filename))
|
||||
{
|
||||
var arguments = parts[1];
|
||||
if (_settings.LeaveShellOpen)
|
||||
{
|
||||
// Wrap the command in a cmd.exe process
|
||||
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{filename} {arguments}\"", runAsVerbArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
info = SetProcessStartInfo(filename, workingDirectory, arguments, runAsVerbArg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_settings.LeaveShellOpen)
|
||||
{
|
||||
// Wrap the command in a cmd.exe process
|
||||
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
info = SetProcessStartInfo(command, verb: runAsVerbArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_settings.LeaveShellOpen)
|
||||
{
|
||||
// Wrap the command in a cmd.exe process
|
||||
info = SetProcessStartInfo("cmd.exe", workingDirectory, $"/k \"{command}\"", runAsVerbArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
info = SetProcessStartInfo(command, verb: runAsVerbArg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
info.UseShellExecute = true;
|
||||
|
||||
_settings.AddCmdHistory(command);
|
||||
|
||||
return info;
|
||||
}
|
||||
else
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Error extracting setting" });
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
try
|
||||
{
|
||||
Execute(Process.Start, PrepareProcessStartInfo(Cmd, _runas));
|
||||
}
|
||||
catch
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "Error starting the process " });
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.CmdPal.Ext.Shell.Helpers;
|
||||
|
||||
public enum RunAsType
|
||||
{
|
||||
None,
|
||||
Administrator,
|
||||
OtherUser,
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Shell.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
|
||||
public class SettingsManager : JsonSettingsManager
|
||||
{
|
||||
private static readonly string _namespace = "shell";
|
||||
|
||||
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
||||
|
||||
private static readonly List<ChoiceSetSetting.Choice> _choices =
|
||||
[
|
||||
new ChoiceSetSetting.Choice(Resources.find_executable_file_and_run_it, "2"), // idk why but this is how PT Run did it? Maybe ordering matters there
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_command_prompt, "0"),
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_powershell, "1"),
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_powershell_seven, "6"),
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_cmd, "5"),
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_powershell, "3"),
|
||||
new ChoiceSetSetting.Choice(Resources.run_command_in_windows_terminal_powershell_seven, "4"),
|
||||
];
|
||||
|
||||
private readonly ToggleSetting _leaveShellOpen = new(
|
||||
Namespaced(nameof(LeaveShellOpen)),
|
||||
Resources.leave_shell_open,
|
||||
Resources.leave_shell_open,
|
||||
false); // TODO -- double check default value
|
||||
|
||||
private readonly ChoiceSetSetting _shellCommandExecution = new(
|
||||
Namespaced(nameof(ShellCommandExecution)),
|
||||
Resources.shell_command_execution,
|
||||
Resources.shell_command_execution_description,
|
||||
_choices);
|
||||
|
||||
public bool LeaveShellOpen => _leaveShellOpen.Value;
|
||||
|
||||
public string ShellCommandExecution => _shellCommandExecution.Value ?? string.Empty;
|
||||
|
||||
public bool RunAsAdministrator { get; set; }
|
||||
|
||||
public Dictionary<string, int> Count { get; } = [];
|
||||
|
||||
public void AddCmdHistory(string cmdName) => Count[cmdName] = Count.TryGetValue(cmdName, out var currentCount) ? currentCount + 1 : 1;
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
// now, the state is just next to the exe
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
Settings.Add(_leaveShellOpen);
|
||||
Settings.Add(_shellCommandExecution);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.Shell.Commands;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
|
||||
public class ShellListPageHelpers
|
||||
{
|
||||
private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times);
|
||||
private readonly SettingsManager _settings;
|
||||
|
||||
public ShellListPageHelpers(SettingsManager settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
private ListItem GetCurrentCmd(string cmd)
|
||||
{
|
||||
ListItem result = new ListItem(new ExecuteItem(cmd, _settings))
|
||||
{
|
||||
Title = cmd,
|
||||
Subtitle = Properties.Resources.cmd_plugin_name + ": " + Properties.Resources.cmd_execute_through_shell,
|
||||
Icon = new IconInfo(string.Empty),
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ListItem> GetHistoryCmds(string cmd, ListItem result)
|
||||
{
|
||||
IEnumerable<ListItem?> history = _settings.Count.Where(o => o.Key.Contains(cmd, StringComparison.CurrentCultureIgnoreCase))
|
||||
.OrderByDescending(o => o.Value)
|
||||
.Select(m =>
|
||||
{
|
||||
if (m.Key == cmd)
|
||||
{
|
||||
// Using CurrentCulture since this is user facing
|
||||
result.Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value);
|
||||
return null;
|
||||
}
|
||||
|
||||
var ret = new ListItem(new ExecuteItem(m.Key, _settings))
|
||||
{
|
||||
Title = m.Key,
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value),
|
||||
Icon = new IconInfo("\uE81C"),
|
||||
};
|
||||
return ret;
|
||||
}).Where(o => o != null).Take(4);
|
||||
return history.Select(o => o!).ToList();
|
||||
}
|
||||
|
||||
public List<ListItem> Query(string query)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
List<ListItem> results = new List<ListItem>();
|
||||
var cmd = query;
|
||||
if (string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
results = ResultsFromlHistory();
|
||||
}
|
||||
else
|
||||
{
|
||||
var queryCmd = GetCurrentCmd(cmd);
|
||||
results.Add(queryCmd);
|
||||
var history = GetHistoryCmds(cmd, queryCmd);
|
||||
results.AddRange(history);
|
||||
}
|
||||
|
||||
foreach (var currItem in results)
|
||||
{
|
||||
currItem.MoreCommands = LoadContextMenus(currItem).ToArray();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public List<CommandContextItem> LoadContextMenus(ListItem listItem)
|
||||
{
|
||||
var resultlist = new List<CommandContextItem>
|
||||
{
|
||||
new(new ExecuteItem(listItem.Title, _settings, RunAsType.Administrator)),
|
||||
new(new ExecuteItem(listItem.Title, _settings, RunAsType.OtherUser )),
|
||||
};
|
||||
|
||||
return resultlist;
|
||||
}
|
||||
|
||||
private List<ListItem> ResultsFromlHistory()
|
||||
{
|
||||
IEnumerable<ListItem> history = _settings.Count.OrderByDescending(o => o.Value)
|
||||
.Select(m => new ListItem(new ExecuteItem(m.Key, _settings))
|
||||
{
|
||||
Title = m.Key,
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
Subtitle = Properties.Resources.cmd_plugin_name + ": " + string.Format(CultureInfo.CurrentCulture, CmdHasBeenExecutedTimes, m.Value),
|
||||
Icon = new IconInfo("\uE81C"),
|
||||
}).Take(5);
|
||||
|
||||
return history.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.Shell</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Content Update="Assets\Run@2x.svg">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Shell.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Pages;
|
||||
|
||||
internal sealed partial class ShellListPage : DynamicListPage
|
||||
{
|
||||
private readonly ShellListPageHelpers _helper;
|
||||
|
||||
public ShellListPage(SettingsManager settingsManager)
|
||||
{
|
||||
Icon = new IconInfo("\uE756");
|
||||
Id = "com.microsoft.cmdpal.shell";
|
||||
Name = Resources.cmd_plugin_name;
|
||||
PlaceholderText = Resources.list_placeholder_text;
|
||||
_helper = new(settingsManager);
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0);
|
||||
|
||||
public override IListItem[] GetItems() => [.. _helper.Query(SearchText)];
|
||||
}
|
||||
279
src/modules/cmdpal/Exts/Microsoft.CmdPal.Ext.Shell/Properties/Resources.Designer.cs
generated
Normal file
@@ -0,0 +1,279 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||
/// </summary>
|
||||
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||
// class via a tool like ResGen or Visual Studio.
|
||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||
// with the /str option, or rebuild your VS project.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the cached ResourceManager instance used by this class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.CmdPal.Ext.Shell.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overrides the current thread's CurrentUICulture property for all
|
||||
/// resource lookups using this strongly typed resource class.
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
public static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error running the command.
|
||||
/// </summary>
|
||||
public static string cmd_command_failed {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_command_failed", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Command not found.
|
||||
/// </summary>
|
||||
public static string cmd_command_not_found {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_command_not_found", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to execute command through command shell.
|
||||
/// </summary>
|
||||
public static string cmd_execute_through_shell {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_execute_through_shell", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to this command has been executed {0} times.
|
||||
/// </summary>
|
||||
public static string cmd_has_been_executed_times {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_has_been_executed_times", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Executes commands (e.g. 'ping', 'cmd').
|
||||
/// </summary>
|
||||
public static string cmd_plugin_description {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_plugin_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run commands.
|
||||
/// </summary>
|
||||
public static string cmd_plugin_name {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_plugin_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run as administrator (Ctrl+Shift+Enter).
|
||||
/// </summary>
|
||||
public static string cmd_run_as_administrator {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_run_as_administrator", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run as different user (Ctrl+Shift+U).
|
||||
/// </summary>
|
||||
public static string cmd_run_as_user {
|
||||
get {
|
||||
return ResourceManager.GetString("cmd_run_as_user", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Find and run the executable file.
|
||||
/// </summary>
|
||||
public static string find_executable_file_and_run_it {
|
||||
get {
|
||||
return ResourceManager.GetString("find_executable_file_and_run_it", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run.
|
||||
/// </summary>
|
||||
public static string generic_run_command {
|
||||
get {
|
||||
return ResourceManager.GetString("generic_run_command", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Keep shell open.
|
||||
/// </summary>
|
||||
public static string leave_shell_open {
|
||||
get {
|
||||
return ResourceManager.GetString("leave_shell_open", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Type the name of a command to run.
|
||||
/// </summary>
|
||||
public static string list_placeholder_text {
|
||||
get {
|
||||
return ResourceManager.GetString("list_placeholder_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in Command Prompt (cmd.exe).
|
||||
/// </summary>
|
||||
public static string run_command_in_command_prompt {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_command_prompt", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in PowerShell (PowerShell.exe).
|
||||
/// </summary>
|
||||
public static string run_command_in_powershell {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_powershell", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in PowerShell 7 (pwsh.exe).
|
||||
/// </summary>
|
||||
public static string run_command_in_powershell_seven {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_powershell_seven", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in Command Prompt (cmd.exe) using Windows Terminal.
|
||||
/// </summary>
|
||||
public static string run_command_in_windows_terminal_cmd {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_windows_terminal_cmd", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in PowerShell (PowerShell.exe) using Windows Terminal.
|
||||
/// </summary>
|
||||
public static string run_command_in_windows_terminal_powershell {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_windows_terminal_powershell", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run in PowerShell 7 (pwsh.exe) using Windows Terminal.
|
||||
/// </summary>
|
||||
public static string run_command_in_windows_terminal_powershell_seven {
|
||||
get {
|
||||
return ResourceManager.GetString("run_command_in_windows_terminal_powershell_seven", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Press Enter to continue.
|
||||
/// </summary>
|
||||
public static string run_plugin_cmd_wait_message {
|
||||
get {
|
||||
return ResourceManager.GetString("run_plugin_cmd_wait_message", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Settings.
|
||||
/// </summary>
|
||||
public static string settings_page_name {
|
||||
get {
|
||||
return ResourceManager.GetString("settings_page_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run commands.
|
||||
/// </summary>
|
||||
public static string shell_command_display_title {
|
||||
get {
|
||||
return ResourceManager.GetString("shell_command_display_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Command execution.
|
||||
/// </summary>
|
||||
public static string shell_command_execution {
|
||||
get {
|
||||
return ResourceManager.GetString("shell_command_execution", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to All entries using the Windows Terminal force the Windows Terminal as the console host regardless of the system settings.
|
||||
/// </summary>
|
||||
public static string shell_command_execution_description {
|
||||
get {
|
||||
return ResourceManager.GetString("shell_command_execution_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Run commands.
|
||||
/// </summary>
|
||||
public static string shell_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("shell_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="cmd_plugin_name" xml:space="preserve">
|
||||
<value>Run commands</value>
|
||||
</data>
|
||||
<data name="cmd_plugin_description" xml:space="preserve">
|
||||
<value>Executes commands (e.g. 'ping', 'cmd')</value>
|
||||
</data>
|
||||
<data name="cmd_has_been_executed_times" xml:space="preserve">
|
||||
<value>this command has been executed {0} times</value>
|
||||
</data>
|
||||
<data name="cmd_execute_through_shell" xml:space="preserve">
|
||||
<value>execute command through command shell</value>
|
||||
</data>
|
||||
<data name="cmd_run_as_administrator" xml:space="preserve">
|
||||
<value>Run as administrator (Ctrl+Shift+Enter)</value>
|
||||
</data>
|
||||
<data name="cmd_command_failed" xml:space="preserve">
|
||||
<value>Error running the command</value>
|
||||
</data>
|
||||
<data name="cmd_command_not_found" xml:space="preserve">
|
||||
<value>Command not found</value>
|
||||
</data>
|
||||
<data name="cmd_run_as_user" xml:space="preserve">
|
||||
<value>Run as different user (Ctrl+Shift+U)</value>
|
||||
</data>
|
||||
<data name="leave_shell_open" xml:space="preserve">
|
||||
<value>Keep shell open</value>
|
||||
</data>
|
||||
<data name="shell_command_execution" xml:space="preserve">
|
||||
<value>Command execution</value>
|
||||
</data>
|
||||
<data name="run_command_in_command_prompt" xml:space="preserve">
|
||||
<value>Run in Command Prompt (cmd.exe)</value>
|
||||
</data>
|
||||
<data name="run_command_in_powershell" xml:space="preserve">
|
||||
<value>Run in PowerShell (PowerShell.exe)</value>
|
||||
</data>
|
||||
<data name="find_executable_file_and_run_it" xml:space="preserve">
|
||||
<value>Find and run the executable file</value>
|
||||
</data>
|
||||
<data name="run_command_in_powershell_seven" xml:space="preserve">
|
||||
<value>Run in PowerShell 7 (pwsh.exe)</value>
|
||||
</data>
|
||||
<data name="run_command_in_windows_terminal_cmd" xml:space="preserve">
|
||||
<value>Run in Command Prompt (cmd.exe) using Windows Terminal</value>
|
||||
</data>
|
||||
<data name="run_command_in_windows_terminal_powershell" xml:space="preserve">
|
||||
<value>Run in PowerShell (PowerShell.exe) using Windows Terminal</value>
|
||||
</data>
|
||||
<data name="run_command_in_windows_terminal_powershell_seven" xml:space="preserve">
|
||||
<value>Run in PowerShell 7 (pwsh.exe) using Windows Terminal</value>
|
||||
</data>
|
||||
<data name="run_plugin_cmd_wait_message" xml:space="preserve">
|
||||
<value>Press Enter to continue</value>
|
||||
<comment>"Enter" means the Enter key on the keyboard.</comment>
|
||||
</data>
|
||||
<data name="shell_command_execution_description" xml:space="preserve">
|
||||
<value>All entries using the Windows Terminal force the Windows Terminal as the console host regardless of the system settings</value>
|
||||
</data>
|
||||
<data name="shell_command_name" xml:space="preserve">
|
||||
<value>Run commands</value>
|
||||
</data>
|
||||
<data name="generic_run_command" xml:space="preserve">
|
||||
<value>Run</value>
|
||||
</data>
|
||||
<data name="settings_page_name" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="list_placeholder_text" xml:space="preserve">
|
||||
<value>Type the name of a command to run</value>
|
||||
</data>
|
||||
<data name="shell_command_display_title" xml:space="preserve">
|
||||
<value>Run commands</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Shell.Pages;
|
||||
using Microsoft.CmdPal.Ext.Shell.Properties;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell;
|
||||
|
||||
public partial class ShellCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly CommandItem _shellPageItem;
|
||||
private readonly SettingsManager _settingsManager = new();
|
||||
private readonly FallbackCommandItem _fallbackItem;
|
||||
|
||||
public ShellCommandsProvider()
|
||||
{
|
||||
Id = "Run";
|
||||
DisplayName = Resources.cmd_plugin_name;
|
||||
Icon = Icons.RunV2;
|
||||
Settings = _settingsManager.Settings;
|
||||
|
||||
_fallbackItem = new FallbackExecuteItem(_settingsManager);
|
||||
|
||||
_shellPageItem = new CommandItem(new ShellListPage(_settingsManager))
|
||||
{
|
||||
Icon = Icons.RunV2,
|
||||
Title = Resources.shell_command_name,
|
||||
Subtitle = Resources.cmd_plugin_description,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_shellPageItem];
|
||||
|
||||
public override IFallbackCommandItem[]? FallbackCommands() => [_fallbackItem];
|
||||
}
|
||||
|
After Width: | Height: | Size: 579 B |