mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 08:29:10 +01:00
Compare commits
7 Commits
jay/LightS
...
leilzh/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5deab723a9 | ||
|
|
14a45e3e8c | ||
|
|
f760ed9d34 | ||
|
|
7d8f64cf3c | ||
|
|
347c3f1efa | ||
|
|
b71bbf89ce | ||
|
|
caa7114e6f |
1
.github/actions/spell-check/allow/code.txt
vendored
1
.github/actions/spell-check/allow/code.txt
vendored
@@ -94,6 +94,7 @@ onefuzzingestionpreparationtool
|
||||
OTP
|
||||
Yubi
|
||||
Yubico
|
||||
Perplexity
|
||||
svgl
|
||||
|
||||
# KEYS
|
||||
|
||||
2
.github/actions/spell-check/allow/names.txt
vendored
2
.github/actions/spell-check/allow/names.txt
vendored
@@ -29,6 +29,8 @@ shortcutguide
|
||||
|
||||
# 8LWXpg is user name but user folder causes a flag
|
||||
LWXpg
|
||||
# 0x6f677548 is user name but user folder causes a flag
|
||||
x6f677548
|
||||
Adoumie
|
||||
Advaith
|
||||
alekhyareddy
|
||||
|
||||
8
.github/actions/spell-check/expect.txt
vendored
8
.github/actions/spell-check/expect.txt
vendored
@@ -306,6 +306,7 @@ CXVIRTUALSCREEN
|
||||
CYSCREEN
|
||||
CYSMICON
|
||||
CYVIRTUALSCREEN
|
||||
Czechia
|
||||
cziplib
|
||||
Dac
|
||||
dacl
|
||||
@@ -330,6 +331,7 @@ Deact
|
||||
debugbreak
|
||||
decryptor
|
||||
Dedup
|
||||
Deduplicator
|
||||
Deeplink
|
||||
DEFAULTBOOTSTRAPPERINSTALLFOLDER
|
||||
DEFAULTCOLOR
|
||||
@@ -435,6 +437,7 @@ EDITSHORTCUTS
|
||||
EDITTEXT
|
||||
EFile
|
||||
ekus
|
||||
emojis
|
||||
ENABLEDELAYEDEXPANSION
|
||||
ENABLEDPOPUP
|
||||
ENABLETAB
|
||||
@@ -799,6 +802,7 @@ KEYBOARDMANAGEREDITORLIBRARYWRAPPER
|
||||
keyboardmanagerstate
|
||||
keyboardmanagerui
|
||||
keyboardtester
|
||||
keycap
|
||||
KEYEVENTF
|
||||
KEYIMAGE
|
||||
keynum
|
||||
@@ -1780,10 +1784,13 @@ UACUI
|
||||
UAL
|
||||
uap
|
||||
UBR
|
||||
UBreak
|
||||
ubrk
|
||||
UCallback
|
||||
ucrt
|
||||
ucrtd
|
||||
uefi
|
||||
UError
|
||||
uesc
|
||||
UFlags
|
||||
UHash
|
||||
@@ -1853,6 +1860,7 @@ VFT
|
||||
vget
|
||||
vgetq
|
||||
viewmodels
|
||||
virama
|
||||
VIRTKEY
|
||||
VIRTUALDESK
|
||||
VISEGRADRELAY
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
name: Manual Batch Issue Deduplication
|
||||
|
||||
on:
|
||||
workflow_dispatch: # Only runs when manually triggered
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
issue_numbers:
|
||||
description: "JSON array of issue numbers to deduplicate (e.g. [101,102,103])"
|
||||
required: true
|
||||
since:
|
||||
description: "Only compare against issues created after this date (ISO 8601, e.g. 2019-05-05T00:00:00Z)"
|
||||
required: false
|
||||
default: "2019-05-05T00:00:00Z"
|
||||
label_as_duplicate:
|
||||
description: "Apply duplicate label if duplicates are found (true/false)"
|
||||
required: false
|
||||
default: "true"
|
||||
|
||||
permissions:
|
||||
models: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
batch-deduplicate:
|
||||
deduplicate:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
issue: ${{ fromJson(github.event.inputs.issue_numbers) }}
|
||||
steps:
|
||||
- name: Batch Deduplicate Issues
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Run GenAI Issue Deduplicator
|
||||
uses: pelikhan/action-genai-issue-dedup@v0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
label-duplicate: "potential duplicate"
|
||||
comment-duplicate: true
|
||||
close-duplicate: false
|
||||
batch-size: 100
|
||||
since: '2019-05-05T00:00:00Z' # Process issues dating back to 2019
|
||||
duplicate-comment-template: "This issue appears to be a duplicate of #{duplicate_issue_number}."
|
||||
# Add other action-specific inputs if needed
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
github_issue: ${{ matrix.issue }}
|
||||
label_as_duplicate: ${{ github.event.inputs.label_as_duplicate }}
|
||||
|
||||
|
||||
@@ -73,4 +73,5 @@ Below are community created plugins that target a website or software. They are
|
||||
| [YubicoOauthOTP](https://github.com/dlnilsson/Community.PowerToys.Run.Plugin.YubicoOauthOTP) | [dlnilsson](https://github.com/dlnilsson) | Display generated codes from OATH accounts stored on the YubiKey in powerToys Run |
|
||||
| [Firefox Bookmark](https://github.com/8LWXpg/PowerToysRun-FirefoxBookmark) | [8LWXpg](https://github.com/8LWXpg) | Open bookmarks in Firefox based browser |
|
||||
| [Linear](https://github.com/vednig/powertoys-linear) | [vednig](https://github.com/vednig) | Create Linear Issues directly from Powertoys Run |
|
||||
| [PerplexitySearchShortcut](https://github.com/0x6f677548/PowerToys-Run-PerplexitySearchShortcut) | [0x6f677548](https://github.com/0x6f677548) | Search Perplexity |
|
||||
| [SpeedTest](https://github.com/ruslanlap/PowerToysRun-SpeedTest) | [ruslanlap](https://github.com/ruslanlap) | One-command internet speed tests with real-time results, modern UI, and shareable links. |
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
@@ -94,6 +95,7 @@ namespace Microsoft.PowerToys.UITest
|
||||
{
|
||||
Task.Delay(1000).Wait();
|
||||
AddScreenShotsToTestResultsDirectory();
|
||||
AddLogFilesToTestResultsDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +600,92 @@ namespace Microsoft.PowerToys.UITest
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies PowerToys log files to test results directory when test fails.
|
||||
/// Renames files to include the directory structure after \PowerToys.
|
||||
/// </summary>
|
||||
protected void AddLogFilesToTestResultsDirectory()
|
||||
{
|
||||
try
|
||||
{
|
||||
var localAppDataLow = Path.Combine(
|
||||
Environment.GetEnvironmentVariable("USERPROFILE") ?? string.Empty,
|
||||
"AppData",
|
||||
"LocalLow",
|
||||
"Microsoft",
|
||||
"PowerToys");
|
||||
|
||||
if (Directory.Exists(localAppDataLow))
|
||||
{
|
||||
CopyLogFilesFromDirectory(localAppDataLow, string.Empty);
|
||||
}
|
||||
|
||||
var localAppData = Path.Combine(
|
||||
Environment.GetEnvironmentVariable("LOCALAPPDATA") ?? string.Empty,
|
||||
"Microsoft",
|
||||
"PowerToys");
|
||||
|
||||
if (Directory.Exists(localAppData))
|
||||
{
|
||||
CopyLogFilesFromDirectory(localAppData, string.Empty);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Don't fail the test if log file copying fails
|
||||
Console.WriteLine($"Failed to copy log files: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recursively copies log files from a directory and renames them with directory structure.
|
||||
/// </summary>
|
||||
/// <param name="sourceDir">Source directory to copy from</param>
|
||||
/// <param name="relativePath">Relative path from PowerToys folder</param>
|
||||
private void CopyLogFilesFromDirectory(string sourceDir, string relativePath)
|
||||
{
|
||||
if (!Directory.Exists(sourceDir))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Process log files in current directory
|
||||
var logFiles = Directory.GetFiles(sourceDir, "*.log");
|
||||
foreach (var logFile in logFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileName = Path.GetFileName(logFile);
|
||||
var fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
var extension = Path.GetExtension(fileName);
|
||||
|
||||
// Create new filename with directory structure
|
||||
var directoryPart = string.IsNullOrEmpty(relativePath) ? string.Empty : relativePath.Replace("\\", "-") + "-";
|
||||
var newFileName = $"{directoryPart}{fileNameWithoutExt}{extension}";
|
||||
|
||||
// Copy file to test results directory with new name
|
||||
var testResultsDir = TestContext.TestResultsDirectory ?? Path.GetTempPath();
|
||||
var destinationPath = Path.Combine(testResultsDir, newFileName);
|
||||
|
||||
File.Copy(logFile, destinationPath, true);
|
||||
TestContext.AddResultFile(destinationPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to copy log file {logFile}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process subdirectories
|
||||
var subdirectories = Directory.GetDirectories(sourceDir);
|
||||
foreach (var subdir in subdirectories)
|
||||
{
|
||||
var dirName = Path.GetFileName(subdir);
|
||||
var newRelativePath = string.IsNullOrEmpty(relativePath) ? dirName : Path.Combine(relativePath, dirName);
|
||||
CopyLogFilesFromDirectory(subdir, newRelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restart scope exe.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Commands;
|
||||
|
||||
public sealed partial class ConfirmableCommand : InvokableCommand
|
||||
{
|
||||
private readonly IInvokableCommand? _command;
|
||||
|
||||
public Func<bool>? IsConfirmationRequired { get; init; }
|
||||
|
||||
public required string ConfirmationTitle { get; init; }
|
||||
|
||||
public required string ConfirmationMessage { get; init; }
|
||||
|
||||
public required IInvokableCommand Command
|
||||
{
|
||||
get => _command!;
|
||||
init
|
||||
{
|
||||
if (_command is INotifyPropChanged oldNotifier)
|
||||
{
|
||||
oldNotifier.PropChanged -= InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
_command = value;
|
||||
|
||||
if (_command is INotifyPropChanged notifier)
|
||||
{
|
||||
notifier.PropChanged += InnerCommand_PropChanged;
|
||||
}
|
||||
|
||||
OnPropertyChanged(nameof(Name));
|
||||
OnPropertyChanged(nameof(Id));
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get => (_command as Command)?.Name ?? base.Name;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Name = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Name = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override string Id
|
||||
{
|
||||
get => (_command as Command)?.Id ?? base.Id;
|
||||
set
|
||||
{
|
||||
var previous = Id;
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Id = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Id = value;
|
||||
}
|
||||
|
||||
if (previous != Id)
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override IconInfo Icon
|
||||
{
|
||||
get => (_command as Command)?.Icon ?? base.Icon;
|
||||
set
|
||||
{
|
||||
if (_command is Command cmd)
|
||||
{
|
||||
cmd.Icon = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
base.Icon = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConfirmableCommand()
|
||||
{
|
||||
// Allow init-only construction
|
||||
}
|
||||
|
||||
[SetsRequiredMembers]
|
||||
public ConfirmableCommand(IInvokableCommand command, string confirmationTitle, string confirmationMessage, Func<bool>? isConfirmationRequired = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(confirmationMessage);
|
||||
ArgumentNullException.ThrowIfNull(confirmationMessage);
|
||||
|
||||
IsConfirmationRequired = isConfirmationRequired;
|
||||
ConfirmationTitle = confirmationTitle;
|
||||
ConfirmationMessage = confirmationMessage;
|
||||
Command = command;
|
||||
}
|
||||
|
||||
private void InnerCommand_PropChanged(object sender, IPropChangedEventArgs args)
|
||||
{
|
||||
var property = args.PropertyName;
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Name))
|
||||
{
|
||||
OnPropertyChanged(nameof(Name));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Id))
|
||||
{
|
||||
OnPropertyChanged(nameof(Id));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(property) || property == nameof(Icon))
|
||||
{
|
||||
OnPropertyChanged(nameof(Icon));
|
||||
}
|
||||
}
|
||||
|
||||
public override ICommandResult Invoke()
|
||||
{
|
||||
var showConfirmationDialog = IsConfirmationRequired?.Invoke() ?? true;
|
||||
if (showConfirmationDialog)
|
||||
{
|
||||
return CommandResult.Confirm(new ConfirmationArgs
|
||||
{
|
||||
Title = ConfirmationTitle,
|
||||
Description = ConfirmationMessage,
|
||||
PrimaryCommand = Command,
|
||||
IsPrimaryCommandCritical = true,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Command.Invoke(this) ?? CommandResult.Dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Common.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Well-known key chords used in the Command Palette and extensions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assigned key chords should not conflict with system or application shortcuts.
|
||||
/// However, the key chords in this class are not guaranteed to be unique and may conflict
|
||||
/// with each other, especially when commands appear together in the same menu.
|
||||
/// </remarks>
|
||||
public static class WellKnownKeyChords
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for opening the file location. Shortcut: Ctrl+Shift+E.
|
||||
/// </summary>
|
||||
public static KeyChord OpenFileLocation { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.E);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for copying the file path. Shortcut: Ctrl+Shift+C.
|
||||
/// </summary>
|
||||
public static KeyChord CopyFilePath { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.C);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for opening the current location in a console. Shortcut: Ctrl+Shift+R.
|
||||
/// </summary>
|
||||
public static KeyChord OpenInConsole { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.R);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for running the selected item as administrator. Shortcut: Ctrl+Shift+Enter.
|
||||
/// </summary>
|
||||
public static KeyChord RunAsAdministrator { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.Enter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for running the selected item as a different user. Shortcut: Ctrl+Shift+U.
|
||||
/// </summary>
|
||||
public static KeyChord RunAsDifferentUser { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: (int)VirtualKey.U);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the well-known key chord for toggling the pin state. Shortcut: Ctrl+P.
|
||||
/// </summary>
|
||||
public static KeyChord TogglePin { get; } = KeyChordHelpers.FromModifiers(ctrl: true, vkey: (int)VirtualKey.P);
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.UI.Deferred;
|
||||
using Microsoft.Terminal.UI;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -55,6 +57,8 @@ public partial class IconBox : ContentControl
|
||||
{
|
||||
TabFocusNavigation = KeyboardNavigationMode.Once;
|
||||
IsTabStop = false;
|
||||
HorizontalContentAlignment = HorizontalAlignment.Center;
|
||||
VerticalContentAlignment = VerticalAlignment.Center;
|
||||
}
|
||||
|
||||
private static void OnSourcePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
@@ -75,6 +79,8 @@ public partial class IconBox : ContentControl
|
||||
IconSourceElement elem = new()
|
||||
{
|
||||
IconSource = fontIco,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
@this.Content = elem;
|
||||
break;
|
||||
@@ -98,14 +104,20 @@ public partial class IconBox : ContentControl
|
||||
else
|
||||
{
|
||||
// TODO GH #239 switch back when using the new MD text block
|
||||
// Switching back to EnqueueAsync has broken icons in tags (they don't show)
|
||||
// _ = @this._queue.EnqueueAsync(() =>
|
||||
@this._queue.TryEnqueue(new(async () =>
|
||||
@this._queue.TryEnqueue(async void () =>
|
||||
{
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
if (@this.SourceRequested is not null)
|
||||
try
|
||||
{
|
||||
if (@this.SourceRequested is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestedTheme = @this.ActualTheme;
|
||||
var eventArgs = new SourceRequestedEventArgs(e.NewValue, requestedTheme);
|
||||
|
||||
await @this.SourceRequested.InvokeAsync(@this, eventArgs);
|
||||
|
||||
// After the await:
|
||||
@@ -130,37 +142,35 @@ public partial class IconBox : ContentControl
|
||||
// So, if the icon we get back was a font icon,
|
||||
// and the glyph for that icon is NOT in the range of
|
||||
// Segoe icons, then let's give the icon some extra space
|
||||
@this.Padding = new Thickness(0);
|
||||
|
||||
IconDataViewModel? iconData = null;
|
||||
if (eventArgs.Key is IconDataViewModel)
|
||||
var iconData = eventArgs.Key switch
|
||||
{
|
||||
iconData = eventArgs.Key as IconDataViewModel;
|
||||
IconDataViewModel key => key,
|
||||
IconInfoViewModel info => requestedTheme == ElementTheme.Light ? info.Light : info.Dark,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
if (iconData?.Icon is not null && @this.Source is FontIconSource)
|
||||
{
|
||||
var iconSize =
|
||||
!double.IsNaN(@this.Width) ? @this.Width :
|
||||
!double.IsNaN(@this.Height) ? @this.Height :
|
||||
@this.ActualWidth > 0 ? @this.ActualWidth :
|
||||
@this.ActualHeight;
|
||||
|
||||
@this.Padding = new Thickness(Math.Round(iconSize * -0.2));
|
||||
}
|
||||
else if (eventArgs.Key is IconInfoViewModel info)
|
||||
else
|
||||
{
|
||||
iconData = requestedTheme == ElementTheme.Light ? info.Light : info.Dark;
|
||||
}
|
||||
|
||||
if (iconData is not null &&
|
||||
@this.Source is FontIconSource)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(iconData.Icon) && iconData.Icon.Length <= 2)
|
||||
{
|
||||
var ch = iconData.Icon[0];
|
||||
|
||||
// The range of MDL2 Icons isn't explicitly defined, but
|
||||
// we're using this based off the table on:
|
||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
||||
var isMDL2Icon = ch is >= '\uE700' and <= '\uF8FF';
|
||||
if (!isMDL2Icon)
|
||||
{
|
||||
@this.Padding = new Thickness(-4);
|
||||
}
|
||||
}
|
||||
@this.Padding = default;
|
||||
}
|
||||
}
|
||||
}));
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception from TryEnqueue bypasses the global error handler,
|
||||
// and crashes the app.
|
||||
Logger.LogError("Failed to set icon", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
#include "pch.h"
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
#include "FontIconGlyphClassifier.g.cpp"
|
||||
|
||||
#include <icu.h>
|
||||
#include <utility>
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Check if the code point is in the Private Use Area range used by Fluent UI icons.
|
||||
[[nodiscard]] constexpr bool _isFluentIconPua(const UChar32 cp) noexcept
|
||||
{
|
||||
static constexpr UChar32 _fluentIconsPrivateUseAreaStart = 0xE700;
|
||||
static constexpr UChar32 _fluentIconsPrivateUseAreaEnd = 0xF8FF;
|
||||
return cp >= _fluentIconsPrivateUseAreaStart && cp <= _fluentIconsPrivateUseAreaEnd;
|
||||
}
|
||||
|
||||
// Determine if the given text (as a sequence of UChar code units) is emoji
|
||||
[[nodiscard]] bool _isEmoji(const UChar* p, const int32_t length) noexcept
|
||||
{
|
||||
if (!p || length < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// https://www.unicode.org/reports/tr51/#Emoji_Variation_Selector_Notes
|
||||
constexpr UChar32 vs15CodePoint = 0xFE0E; // Variation Selectors 15: text variation selector
|
||||
constexpr UChar32 vs16CodePoint = 0xFE0F; // Variation Selectors: 16 emoji variation selector
|
||||
|
||||
// Decode the first code point correctly (surrogate-safe)
|
||||
int32_t i0{ 0 };
|
||||
UChar32 first{ 0 };
|
||||
U16_NEXT(p, i0, length, first);
|
||||
|
||||
for (int32_t i = 0; i < length;)
|
||||
{
|
||||
UChar32 cp{ 0 };
|
||||
U16_NEXT(p, i, length, cp);
|
||||
|
||||
if (cp == vs16CodePoint) { return true; }
|
||||
if (cp == vs15CodePoint) { return false; }
|
||||
}
|
||||
|
||||
return !U_IS_SURROGATE(first) && u_hasBinaryProperty(first, UCHAR_EMOJI_PRESENTATION);
|
||||
}
|
||||
}
|
||||
|
||||
bool FontIconGlyphClassifier::IsLikelyToBeEmojiOrSymbolIcon(const hstring& text)
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.size() == 1 && !IS_HIGH_SURROGATE(text[0]))
|
||||
{
|
||||
// If it's a single code unit, it's definitely either zero or one grapheme clusters.
|
||||
// If it turns out to be illegal Unicode, we don't really care.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (text.size() >= 2 && text[0] <= 0x7F && text[1] <= 0x7F)
|
||||
{
|
||||
// Two adjacent ASCII characters (as seen in most file paths) aren't a single
|
||||
// grapheme cluster.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use ICU to determine whether text is composed of a single grapheme cluster.
|
||||
int32_t off{ 0 };
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* const bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
reinterpret_cast<const UChar*>(text.data()),
|
||||
static_cast<int>(text.size()),
|
||||
&status) };
|
||||
if (bi)
|
||||
{
|
||||
if (U_SUCCESS(status))
|
||||
{
|
||||
off = ubrk_next(bi);
|
||||
}
|
||||
ubrk_close(bi);
|
||||
}
|
||||
return std::cmp_equal(off, text.size());
|
||||
}
|
||||
|
||||
FontIconGlyphKind FontIconGlyphClassifier::Classify(hstring const& text) noexcept
|
||||
{
|
||||
if (text.empty())
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
const size_t textSize{ text.size() };
|
||||
const auto* buffer{ reinterpret_cast<const UChar*>(text.c_str()) };
|
||||
|
||||
// Fast path 1: Single UTF-16 code unit (most common case)
|
||||
if (textSize == 1)
|
||||
{
|
||||
const UChar ch{ buffer[0] };
|
||||
|
||||
if (IS_HIGH_SURROGATE(ch))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
if (_isFluentIconPua(ch))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(&ch, 1))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
|
||||
// Fast path 2: Common file path pattern - two ASCII printable characters
|
||||
if (textSize >= 2 && buffer[0] <= 0x7F && buffer[1] <= 0x7F)
|
||||
{
|
||||
// Definitely multiple graphemes
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Expensive path: Use ICU to determine grapheme boundaries
|
||||
UErrorCode status{ U_ZERO_ERROR };
|
||||
|
||||
UBreakIterator* bi{ ubrk_open(UBRK_CHARACTER,
|
||||
nullptr,
|
||||
buffer,
|
||||
static_cast<int32_t>(textSize),
|
||||
&status) };
|
||||
|
||||
if (U_FAILURE(status) || !bi)
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
const int32_t start{ ubrk_first(bi) };
|
||||
const int32_t end{ ubrk_next(bi) }; // end of first grapheme
|
||||
ubrk_close(bi);
|
||||
|
||||
// No graphemes found
|
||||
if (end == UBRK_DONE || end <= start)
|
||||
{
|
||||
return FontIconGlyphKind::None;
|
||||
}
|
||||
|
||||
// If there's more than one grapheme, it's not a valid icon glyph
|
||||
if (std::cmp_not_equal(end, textSize))
|
||||
{
|
||||
return FontIconGlyphKind::Invalid;
|
||||
}
|
||||
|
||||
// Exactly one grapheme: classify
|
||||
const UChar* grapheme = buffer + start;
|
||||
const int32_t graphemeLength = end - start;
|
||||
|
||||
if (graphemeLength == 1 && _isFluentIconPua(grapheme[0]))
|
||||
{
|
||||
return FontIconGlyphKind::FluentSymbol;
|
||||
}
|
||||
|
||||
if (_isEmoji(grapheme, graphemeLength))
|
||||
{
|
||||
return FontIconGlyphKind::Emoji;
|
||||
}
|
||||
|
||||
return FontIconGlyphKind::Other;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "FontIconGlyphClassifier.g.h"
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
struct FontIconGlyphClassifier
|
||||
{
|
||||
[[nodiscard]] static bool IsLikelyToBeEmojiOrSymbolIcon(const winrt::hstring& text);
|
||||
|
||||
[[nodiscard]] static FontIconGlyphKind Classify(winrt::hstring const& text) noexcept;
|
||||
};
|
||||
}
|
||||
|
||||
namespace winrt::Microsoft::Terminal::UI::factory_implementation
|
||||
{
|
||||
BASIC_FACTORY(FontIconGlyphClassifier);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Microsoft.Terminal.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// Categorizes the type of a single grapheme cluster or input text.
|
||||
/// Used to determine how the input should be handled or rendered (for example,
|
||||
/// whether it should be treated as an emoji, an icon from a symbol font, plain text, etc.).
|
||||
/// </summary>
|
||||
enum FontIconGlyphKind
|
||||
{
|
||||
/// <summary>
|
||||
/// Input is invalid or contains more than one grapheme cluster and therefore cannot be
|
||||
/// treated as a single symbol. Typical for multi-character text like file paths
|
||||
/// or composed strings that include separators.
|
||||
/// </summary>
|
||||
Invalid = -1,
|
||||
|
||||
/// <summary>
|
||||
/// No grapheme present (empty string). Indicates absence of a symbol.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// A single emoji grapheme cluster. This may consist of multiple Unicode code
|
||||
/// points combined into one visible glyph (e.g., emoji with modifiers or ZWJ sequences).
|
||||
/// </summary>
|
||||
Emoji = 1,
|
||||
|
||||
/// <summary>
|
||||
/// A single glyph from the Segoe Fluent Icons / MDL2 Assets Private Use Area (PUA),
|
||||
/// typically in the Unicode range U+E700–U+F8FF. These are font-based icons (Fluent/MDL2).
|
||||
/// </summary>
|
||||
FluentSymbol = 2,
|
||||
|
||||
/// <summary>
|
||||
/// A single non-emoji grapheme that is not a Fluent/MDL2 PUA symbol.
|
||||
/// Covers ordinary characters, letters, numbers, or other single glyph symbols.
|
||||
/// </summary>
|
||||
Other = 3,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Static utility class for text and icon analysis
|
||||
/// </summary>
|
||||
static runtimeclass FontIconGlyphClassifier
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines if text represents a single grapheme cluster (emoji/symbol icon).
|
||||
/// Uses ICU for Unicode boundary detection to distinguish icons from file paths.
|
||||
/// </summary>
|
||||
/// <param name="text">Text to analyze</param>
|
||||
/// <returns>True if single grapheme cluster, false for multi-character text or paths</returns>
|
||||
static Boolean IsLikelyToBeEmojiOrSymbolIcon(String text);
|
||||
|
||||
/// <summary>
|
||||
/// Classifies the input into a glyph kind suitable for icon or text rendering.
|
||||
/// </summary>
|
||||
static FontIconGlyphKind Classify(String text);
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "IconPathConverter.h"
|
||||
#include "IconPathConverter.g.cpp"
|
||||
|
||||
// #include "Utils.h"
|
||||
#include "FontIconGlyphClassifier.h"
|
||||
|
||||
#include <Shlobj.h>
|
||||
#include <Shlobj_core.h>
|
||||
@@ -110,7 +110,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
if (til::equals_insensitive_ascii(iconUri.Extension(), L".svg"))
|
||||
{
|
||||
typename ImageIconSource<TIconSource>::type iconSource;
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
winrt::Microsoft::UI::Xaml::Media::Imaging::SvgImageSource source{ iconUri };
|
||||
iconSource.ImageSource(source);
|
||||
return iconSource;
|
||||
}
|
||||
@@ -169,41 +169,46 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
// If we fail to set the icon source using the "icon" as a path,
|
||||
// let's try it as a symbol/emoji.
|
||||
//
|
||||
// Anything longer than 2 wchar_t's _isn't_ an emoji or symbol, so
|
||||
// don't do this if it's just an invalid path.
|
||||
if (!iconSource && iconPath.size() <= 2)
|
||||
if (!iconSource)
|
||||
{
|
||||
try
|
||||
{
|
||||
typename FontIconSource<TIconSource>::type icon;
|
||||
const auto ch = til::at(iconPath, 0);
|
||||
const auto glyph_kind = FontIconGlyphClassifier::Classify(iconPath);
|
||||
|
||||
// The range of MDL2 Icons isn't explicitly defined, but
|
||||
// we're using this based off the table on:
|
||||
// https://docs.microsoft.com/en-us/windows/uwp/design/style/segoe-ui-symbol-font
|
||||
const auto isMDL2Icon = ch >= L'\uE700' && ch <= L'\uF8FF';
|
||||
if (isMDL2Icon)
|
||||
winrt::hstring family;
|
||||
if (glyph_kind == FontIconGlyphKind::Invalid)
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
||||
family = L"Segoe UI";
|
||||
}
|
||||
else if (!fontFamily.empty())
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
|
||||
|
||||
family = fontFamily;
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::FluentSymbol)
|
||||
{
|
||||
family = L"Segoe Fluent Icons, Segoe MDL2 Assets";
|
||||
}
|
||||
else if (glyph_kind == FontIconGlyphKind::Emoji)
|
||||
{
|
||||
// Emoji and other symbols go in the Segoe UI Emoji font.
|
||||
// Some emojis (e.g. 2️⃣) would be rendered as emoji glyphs otherwise.
|
||||
family = L"Segoe UI Emoji, Segoe UI";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: you _do_ need to manually set the font here.
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe UI" });
|
||||
family = L"Segoe UI";
|
||||
}
|
||||
|
||||
typename FontIconSource<TIconSource>::type icon;
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ family });
|
||||
icon.FontSize(targetSize);
|
||||
icon.Glyph(iconPath);
|
||||
icon.Glyph(glyph_kind == FontIconGlyphKind::Invalid ? L"\u25CC" : iconPath);
|
||||
iconSource = icon;
|
||||
}
|
||||
CATCH_LOG();
|
||||
}
|
||||
}
|
||||
|
||||
if (!iconSource)
|
||||
{
|
||||
// Set the default IconSource to a BitmapIconSource with a null source
|
||||
@@ -326,7 +331,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
}
|
||||
|
||||
static winrt::Microsoft::UI::Xaml::Media::Imaging::SoftwareBitmapSource _getImageIconSourceForBinary(std::wstring_view iconPathWithoutIndex,
|
||||
int index,
|
||||
int index,
|
||||
int targetSize)
|
||||
{
|
||||
// Try:
|
||||
|
||||
@@ -159,6 +159,9 @@
|
||||
<ClInclude Include="ResourceString.h">
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
<ClInclude Include="FontIconGlyphClassifier.h">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="init.cpp" />
|
||||
@@ -178,6 +181,9 @@
|
||||
<DependentUpon>ResourceString.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
|
||||
<ClCompile Include="FontIconGlyphClassifier.cpp">
|
||||
<DependentUpon>FontIconGlyphClassifier.idl</DependentUpon>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Midl Include="Converters.idl" />
|
||||
@@ -185,6 +191,7 @@
|
||||
<Midl Include="RunHistory.idl" />
|
||||
<Midl Include="IDirectKeyListener.idl" />
|
||||
<Midl Include="ResourceString.idl" />
|
||||
<Midl Include="FontIconGlyphClassifier.idl" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// 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 Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||
|
||||
@@ -13,34 +12,22 @@ public class MockSettingsInterface : ISettingsInterface
|
||||
{
|
||||
private readonly List<HistoryItem> _historyItems;
|
||||
|
||||
public event EventHandler HistoryChanged;
|
||||
|
||||
public bool GlobalIfURI { get; set; }
|
||||
|
||||
public uint HistoryItemCount { get; set; }
|
||||
public int HistoryItemCount { get; set; }
|
||||
|
||||
public MockSettingsInterface(uint historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||
public IReadOnlyList<HistoryItem> HistoryItems => _historyItems;
|
||||
|
||||
public MockSettingsInterface(int historyItemCount = 0, bool globalIfUri = true, List<HistoryItem> mockHistory = null)
|
||||
{
|
||||
_historyItems = mockHistory ?? new List<HistoryItem>();
|
||||
GlobalIfURI = globalIfUri;
|
||||
HistoryItemCount = historyItemCount;
|
||||
}
|
||||
|
||||
public List<ListItem> LoadHistory()
|
||||
{
|
||||
var listItems = new List<ListItem>();
|
||||
foreach (var historyItem in _historyItems)
|
||||
{
|
||||
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
|
||||
{
|
||||
Title = historyItem.SearchString,
|
||||
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
|
||||
});
|
||||
}
|
||||
|
||||
listItems.Reverse();
|
||||
return listItems;
|
||||
}
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem)
|
||||
public void AddHistoryItem(HistoryItem historyItem)
|
||||
{
|
||||
if (historyItem is null)
|
||||
{
|
||||
@@ -54,15 +41,18 @@ public class MockSettingsInterface : ISettingsInterface
|
||||
{
|
||||
while (_historyItems.Count > HistoryItemCount)
|
||||
{
|
||||
_historyItems.RemoveAt(0); // Remove the oldest item
|
||||
_historyItems.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Helper method for testing
|
||||
public void ClearHistory()
|
||||
{
|
||||
_historyItems.Clear();
|
||||
HistoryChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
// Helper method for testing
|
||||
|
||||
@@ -45,7 +45,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadHistoryReturnsExpectedItems()
|
||||
public async Task HistoryReturnsExpectedItems()
|
||||
{
|
||||
// Setup
|
||||
var mockHistoryItems = new List<HistoryItem>
|
||||
@@ -77,7 +77,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadHistoryMoreThanLimitation()
|
||||
public async Task HistoryExceedingLimitReturnsMaxItems()
|
||||
{
|
||||
// Setup
|
||||
var mockHistoryItems = new List<HistoryItem>
|
||||
@@ -109,7 +109,7 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task LoadHistoryWithDisableSetting()
|
||||
public async Task HistoryWhenSetToNoneReturnEmptyList()
|
||||
{
|
||||
// Setup
|
||||
var mockHistoryItems = new List<HistoryItem>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Threading.Tasks;
|
||||
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class SettingsManagerTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task HistoryChangedEventIsRaisedWhenItemIsAdded()
|
||||
{
|
||||
// Setup
|
||||
var settings = new MockSettingsInterface(historyItemCount: 5);
|
||||
var page = new WebSearchListPage(settings);
|
||||
|
||||
var eventRaised = false;
|
||||
|
||||
try
|
||||
{
|
||||
settings.HistoryChanged += Handler;
|
||||
|
||||
// Act
|
||||
settings.AddHistoryItem(new HistoryItem("test event", DateTime.UtcNow));
|
||||
await Task.Delay(50);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(eventRaised, "Expected HistoryChanged to be raised when saving history.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
settings.HistoryChanged -= Handler;
|
||||
page.Dispose();
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void Handler(object s, EventArgs e) => eventRaised = true;
|
||||
}
|
||||
}
|
||||
@@ -133,17 +133,13 @@ internal sealed partial class AppListItem : ListItem
|
||||
|
||||
newCommands.Add(new Separator());
|
||||
|
||||
// 0x50 = P
|
||||
// Full key chord would be Ctrl+P
|
||||
var pinKeyChord = KeyChordHelpers.FromModifiers(true, false, false, false, 0x50, 0);
|
||||
|
||||
if (isPinned)
|
||||
{
|
||||
newCommands.Add(
|
||||
new CommandContextItem(
|
||||
new UnpinAppCommand(this.AppIdentifier))
|
||||
{
|
||||
RequestedShortcut = pinKeyChord,
|
||||
RequestedShortcut = KeyChords.TogglePin,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -152,7 +148,7 @@ internal sealed partial class AppListItem : ListItem
|
||||
new CommandContextItem(
|
||||
new PinAppCommand(this.AppIdentifier))
|
||||
{
|
||||
RequestedShortcut = pinKeyChord,
|
||||
RequestedShortcut = KeyChords.TogglePin,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
internal static class KeyChords
|
||||
{
|
||||
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
|
||||
|
||||
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
|
||||
|
||||
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
|
||||
|
||||
internal static KeyChord RunAsAdministrator { get; } = WellKnownKeyChords.RunAsAdministrator;
|
||||
|
||||
internal static KeyChord RunAsDifferentUser { get; } = WellKnownKeyChords.RunAsDifferentUser;
|
||||
|
||||
internal static KeyChord TogglePin { get; } = WellKnownKeyChords.TogglePin;
|
||||
}
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
|
||||
<ProjectReference Include="..\..\Microsoft.CmdPal.Common\Microsoft.CmdPal.Common.csproj" />
|
||||
<!-- CmdPal Toolkit reference now included via Common.ExtDependencies.props -->
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ public class UWPApplication : IUWPApplication
|
||||
new CommandContextItem(
|
||||
new RunAsAdminCommand(UniqueIdentifier, string.Empty, true))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
|
||||
RequestedShortcut = KeyChords.RunAsAdministrator,
|
||||
});
|
||||
|
||||
// We don't add context menu to 'run as different user', because UWP applications normally installed per user and not for all users.
|
||||
@@ -97,7 +97,7 @@ public class UWPApplication : IUWPApplication
|
||||
new CommandContextItem(
|
||||
new CopyTextCommand(Location) { Name = Resources.copy_path })
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
|
||||
RequestedShortcut = KeyChords.CopyFilePath,
|
||||
});
|
||||
|
||||
commands.Add(
|
||||
@@ -107,14 +107,14 @@ public class UWPApplication : IUWPApplication
|
||||
Name = Resources.open_containing_folder,
|
||||
})
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
});
|
||||
|
||||
commands.Add(
|
||||
new CommandContextItem(
|
||||
new OpenInConsoleCommand(Package.Location))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
||||
RequestedShortcut = KeyChords.OpenInConsole,
|
||||
});
|
||||
|
||||
commands.Add(
|
||||
|
||||
@@ -191,32 +191,32 @@ public class Win32Program : IProgram
|
||||
commands.Add(new CommandContextItem(
|
||||
new RunAsAdminCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory, false))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Enter),
|
||||
RequestedShortcut = KeyChords.RunAsAdministrator,
|
||||
});
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new RunAsUserCommand(!string.IsNullOrEmpty(LnkFilePath) ? LnkFilePath : FullPath, ParentDirectory))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.U),
|
||||
RequestedShortcut = KeyChords.RunAsDifferentUser,
|
||||
});
|
||||
}
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new CopyTextCommand(FullPath) { Name = Resources.copy_path })
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.C),
|
||||
RequestedShortcut = KeyChords.CopyFilePath,
|
||||
});
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new OpenPathCommand(ParentDirectory))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.E),
|
||||
RequestedShortcut = KeyChords.OpenFileLocation,
|
||||
});
|
||||
|
||||
commands.Add(new CommandContextItem(
|
||||
new OpenInConsoleCommand(ParentDirectory))
|
||||
{
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.R),
|
||||
RequestedShortcut = KeyChords.OpenInConsole,
|
||||
});
|
||||
|
||||
if (AppType == ApplicationType.ShortcutApplication || AppType == ApplicationType.ApprefApplication || AppType == ApplicationType.Win32Application)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -11,19 +12,25 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
public partial class ClipboardHistoryCommandsProvider : CommandProvider
|
||||
{
|
||||
private readonly ListItem _clipboardHistoryListItem;
|
||||
private readonly SettingsManager _settingsManager = new();
|
||||
|
||||
public ClipboardHistoryCommandsProvider()
|
||||
{
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage())
|
||||
_clipboardHistoryListItem = new ListItem(new ClipboardHistoryListPage(_settingsManager))
|
||||
{
|
||||
Title = Properties.Resources.list_item_title,
|
||||
Subtitle = Properties.Resources.list_item_subtitle,
|
||||
Icon = Icons.ClipboardListIcon,
|
||||
MoreCommands = [
|
||||
new CommandContextItem(_settingsManager.Settings.SettingsPage),
|
||||
],
|
||||
};
|
||||
|
||||
DisplayName = Properties.Resources.provider_display_name;
|
||||
Icon = Icons.ClipboardListIcon;
|
||||
Id = "Windows.ClipboardHistory";
|
||||
|
||||
Settings = _settingsManager.Settings;
|
||||
}
|
||||
|
||||
public override IListItem[] TopLevelCommands()
|
||||
|
||||
@@ -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.
|
||||
|
||||
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 DeleteItemCommand : InvokableCommand
|
||||
{
|
||||
private readonly ClipboardItem _clipboardItem;
|
||||
|
||||
internal DeleteItemCommand(ClipboardItem clipboardItem)
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
Name = Properties.Resources.delete_command_name;
|
||||
Icon = Icons.DeleteIcon;
|
||||
}
|
||||
|
||||
public override CommandResult Invoke()
|
||||
{
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
return CommandResult.ShowToast(new ToastArgs
|
||||
{
|
||||
Message = Properties.Resources.delete_toast_text,
|
||||
Result = CommandResult.KeepOpen(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.CmdPal.Common.Messages;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
@@ -14,11 +15,13 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
{
|
||||
private readonly ClipboardItem _clipboardItem;
|
||||
private readonly ClipboardFormat _clipboardFormat;
|
||||
private readonly ISettingOptions _settings;
|
||||
|
||||
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat)
|
||||
internal PasteCommand(ClipboardItem clipboardItem, ClipboardFormat clipboardFormat, ISettingOptions settings)
|
||||
{
|
||||
_clipboardItem = clipboardItem;
|
||||
_clipboardFormat = clipboardFormat;
|
||||
_settings = settings;
|
||||
Name = Properties.Resources.paste_command_name;
|
||||
Icon = Icons.PasteIcon;
|
||||
}
|
||||
@@ -39,7 +42,11 @@ internal sealed partial class PasteCommand : InvokableCommand
|
||||
|
||||
ClipboardHelper.SendPasteKeyCombination();
|
||||
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
if (!_settings.KeepAfterPaste)
|
||||
{
|
||||
Clipboard.DeleteItemFromHistory(_clipboardItem.Item);
|
||||
}
|
||||
|
||||
return CommandResult.ShowToast(Properties.Resources.paste_toast_text);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.ClipboardHistory.Helpers;
|
||||
|
||||
public interface ISettingOptions
|
||||
{
|
||||
bool KeepAfterPaste { get; }
|
||||
|
||||
bool DeleteFromHistoryRequiresConfirmation { get; }
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
|
||||
internal sealed class SettingsManager : JsonSettingsManager, ISettingOptions
|
||||
{
|
||||
private const string Namespace = "clipboardHistory";
|
||||
|
||||
private static string Namespaced(string propertyName) => $"{Namespace}.{propertyName}";
|
||||
|
||||
private readonly ToggleSetting _keepAfterPaste = new(
|
||||
Namespaced(nameof(KeepAfterPaste)),
|
||||
Resources.settings_keep_after_paste_title!,
|
||||
Resources.settings_keep_after_paste_description!,
|
||||
false);
|
||||
|
||||
private readonly ToggleSetting _confirmDelete = new(
|
||||
Namespaced(nameof(DeleteFromHistoryRequiresConfirmation)),
|
||||
Resources.settings_confirm_delete_title!,
|
||||
Resources.settings_confirm_delete_description!,
|
||||
true);
|
||||
|
||||
public bool KeepAfterPaste => _keepAfterPaste.Value;
|
||||
|
||||
public bool DeleteFromHistoryRequiresConfirmation => _confirmDelete.Value;
|
||||
|
||||
private 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(_keepAfterPaste);
|
||||
Settings.Add(_confirmDelete);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||
}
|
||||
}
|
||||
@@ -14,5 +14,7 @@ internal sealed class Icons
|
||||
|
||||
internal static IconInfo PasteIcon { get; } = new("\uE77F");
|
||||
|
||||
internal static IconInfo DeleteIcon { get; } = new("\uE74D");
|
||||
|
||||
internal static IconInfo ClipboardListIcon { get; } = IconHelpers.FromRelativePath("Assets\\ClipboardHistory.svg");
|
||||
}
|
||||
|
||||
@@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.System;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.ClipboardHistory;
|
||||
|
||||
internal static class KeyChords
|
||||
{
|
||||
internal static KeyChord DeleteEntry { get; } = KeyChordHelpers.FromModifiers(ctrl: true, shift: true, vkey: VirtualKey.Delete);
|
||||
}
|
||||
@@ -7,7 +7,9 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.CmdPal.Common.Commands;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Commands;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using Windows.Storage.Streams;
|
||||
@@ -16,9 +18,11 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
|
||||
public class ClipboardItem
|
||||
{
|
||||
public string? Content { get; set; }
|
||||
public string? Content { get; init; }
|
||||
|
||||
public required ClipboardHistoryItem Item { get; set; }
|
||||
public required ClipboardHistoryItem Item { get; init; }
|
||||
|
||||
public required ISettingOptions Settings { get; init; }
|
||||
|
||||
public DateTimeOffset Timestamp => Item?.Timestamp ?? DateTimeOffset.MinValue;
|
||||
|
||||
@@ -87,6 +91,19 @@ public class ClipboardItem
|
||||
Data = new DetailsLink(Item.Timestamp.DateTime.ToString(DateTimeFormatInfo.CurrentInfo)),
|
||||
});
|
||||
|
||||
var deleteConfirmationCommand = new ConfirmableCommand()
|
||||
{
|
||||
Command = new DeleteItemCommand(this),
|
||||
ConfirmationTitle = Properties.Resources.delete_confirmation_title!,
|
||||
ConfirmationMessage = Properties.Resources.delete_confirmation_message!,
|
||||
IsConfirmationRequired = () => Settings.DeleteFromHistoryRequiresConfirmation,
|
||||
};
|
||||
var deleteContextMenuItem = new CommandContextItem(deleteConfirmationCommand)
|
||||
{
|
||||
IsCritical = true,
|
||||
RequestedShortcut = KeyChords.DeleteEntry,
|
||||
};
|
||||
|
||||
if (IsImage)
|
||||
{
|
||||
var iconData = new IconData(ImageData);
|
||||
@@ -103,7 +120,9 @@ public class ClipboardItem
|
||||
Metadata = metadata.ToArray(),
|
||||
},
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image))
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Image, Settings)),
|
||||
new Separator(),
|
||||
deleteContextMenuItem,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -126,8 +145,10 @@ public class ClipboardItem
|
||||
Metadata = metadata.ToArray(),
|
||||
},
|
||||
MoreCommands = [
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text)),
|
||||
],
|
||||
new CommandContextItem(new PasteCommand(this, ClipboardFormat.Text, Settings)),
|
||||
new Separator(),
|
||||
deleteContextMenuItem,
|
||||
],
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Helpers;
|
||||
using Microsoft.CmdPal.Ext.ClipboardHistory.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
@@ -17,11 +18,15 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Pages;
|
||||
|
||||
internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
{
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly ObservableCollection<ClipboardItem> clipboardHistory;
|
||||
private readonly string _defaultIconPath;
|
||||
|
||||
public ClipboardHistoryListPage()
|
||||
public ClipboardHistoryListPage(SettingsManager settingsManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||
|
||||
_settingsManager = settingsManager;
|
||||
clipboardHistory = [];
|
||||
_defaultIconPath = string.Empty;
|
||||
Icon = Icons.ClipboardListIcon;
|
||||
@@ -84,11 +89,11 @@ internal sealed partial class ClipboardHistoryListPage : ListPage
|
||||
if (item.Content.Contains(StandardDataFormats.Text))
|
||||
{
|
||||
var text = await item.Content.GetTextAsync();
|
||||
items.Add(new ClipboardItem { Content = text, Item = item });
|
||||
items.Add(new ClipboardItem { Settings = _settingsManager, Content = text, Item = item });
|
||||
}
|
||||
else if (item.Content.Contains(StandardDataFormats.Bitmap))
|
||||
{
|
||||
items.Add(new ClipboardItem { Item = item });
|
||||
items.Add(new ClipboardItem { Settings = _settingsManager, Item = item });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,6 +96,42 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Delete.
|
||||
/// </summary>
|
||||
public static string delete_command_name {
|
||||
get {
|
||||
return ResourceManager.GetString("delete_command_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Are you sure you want to delete this item from clipboard history? This action cannot be undone..
|
||||
/// </summary>
|
||||
public static string delete_confirmation_message {
|
||||
get {
|
||||
return ResourceManager.GetString("delete_confirmation_message", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Delete item?.
|
||||
/// </summary>
|
||||
public static string delete_confirmation_title {
|
||||
get {
|
||||
return ResourceManager.GetString("delete_confirmation_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Deleted from clipboard history.
|
||||
/// </summary>
|
||||
public static string delete_toast_text {
|
||||
get {
|
||||
return ResourceManager.GetString("delete_toast_text", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Copy, paste, and search items on the clipboard.
|
||||
/// </summary>
|
||||
@@ -140,5 +176,41 @@ namespace Microsoft.CmdPal.Ext.ClipboardHistory.Properties {
|
||||
return ResourceManager.GetString("provider_display_name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// </summary>
|
||||
public static string settings_confirm_delete_description {
|
||||
get {
|
||||
return ResourceManager.GetString("settings_confirm_delete_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Show a confirmation dialog when manually deleting an item.
|
||||
/// </summary>
|
||||
public static string settings_confirm_delete_title {
|
||||
get {
|
||||
return ResourceManager.GetString("settings_confirm_delete_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to .
|
||||
/// </summary>
|
||||
public static string settings_keep_after_paste_description {
|
||||
get {
|
||||
return ResourceManager.GetString("settings_keep_after_paste_description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Keep items in clipboard history after pasting.
|
||||
/// </summary>
|
||||
public static string settings_keep_after_paste_title {
|
||||
get {
|
||||
return ResourceManager.GetString("settings_keep_after_paste_title", resourceCulture);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,4 +144,28 @@
|
||||
<data name="clipboard_failed_to_load" xml:space="preserve">
|
||||
<value>Loading clipboard history failed</value>
|
||||
</data>
|
||||
<data name="delete_command_name" xml:space="preserve">
|
||||
<value>Delete</value>
|
||||
</data>
|
||||
<data name="delete_toast_text" xml:space="preserve">
|
||||
<value>Deleted from clipboard history</value>
|
||||
</data>
|
||||
<data name="settings_keep_after_paste_description" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="settings_keep_after_paste_title" xml:space="preserve">
|
||||
<value>Keep items in clipboard history after pasting</value>
|
||||
</data>
|
||||
<data name="settings_confirm_delete_title" xml:space="preserve">
|
||||
<value>Show a confirmation dialog when manually deleting an item</value>
|
||||
</data>
|
||||
<data name="settings_confirm_delete_description" xml:space="preserve">
|
||||
<value />
|
||||
</data>
|
||||
<data name="delete_confirmation_title" xml:space="preserve">
|
||||
<value>Delete item?</value>
|
||||
</data>
|
||||
<data name="delete_confirmation_message" xml:space="preserve">
|
||||
<value>Are you sure you want to delete this item from clipboard history? This action cannot be undone.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -91,9 +91,9 @@ internal sealed partial class IndexerListItem : ListItem
|
||||
}
|
||||
|
||||
commands.Add(new CommandContextItem(new OpenWithCommand(fullPath)));
|
||||
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }));
|
||||
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }));
|
||||
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)));
|
||||
commands.Add(new CommandContextItem(new ShowFileInFolderCommand(fullPath) { Name = Resources.Indexer_Command_ShowInFolder }) { RequestedShortcut = KeyChords.OpenFileLocation });
|
||||
commands.Add(new CommandContextItem(new CopyPathCommand(fullPath) { Name = Resources.Indexer_Command_CopyPath }) { RequestedShortcut = KeyChords.CopyFilePath });
|
||||
commands.Add(new CommandContextItem(new OpenInConsoleCommand(fullPath)) { RequestedShortcut = KeyChords.OpenInConsole });
|
||||
commands.Add(new CommandContextItem(new OpenPropertiesCommand(fullPath)));
|
||||
|
||||
if (IsActionsFeatureEnabled && ApiInformation.IsApiContractPresent("Windows.AI.Actions.ActionsContract", 4))
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.Common.Helpers;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Indexer;
|
||||
|
||||
internal static class KeyChords
|
||||
{
|
||||
internal static KeyChord OpenFileLocation { get; } = WellKnownKeyChords.OpenFileLocation;
|
||||
|
||||
internal static KeyChord CopyFilePath { get; } = WellKnownKeyChords.CopyFilePath;
|
||||
|
||||
internal static KeyChord OpenInConsole { get; } = WellKnownKeyChords.OpenInConsole;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ internal sealed partial class SearchWebCommand : InvokableCommand
|
||||
|
||||
if (_settingsManager.HistoryItemCount != 0)
|
||||
{
|
||||
_settingsManager.SaveHistory(new HistoryItem(Arguments, DateTime.Now));
|
||||
_settingsManager.AddHistoryItem(new HistoryItem(Arguments, DateTime.Now));
|
||||
}
|
||||
|
||||
return CommandResult.Dismiss();
|
||||
|
||||
@@ -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;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
internal sealed class HistoryStore
|
||||
{
|
||||
private readonly string _filePath;
|
||||
private readonly List<HistoryItem> _items = [];
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
private int _capacity;
|
||||
|
||||
public event EventHandler? Changed;
|
||||
|
||||
public HistoryStore(string filePath, int capacity)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(filePath);
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
|
||||
_filePath = filePath;
|
||||
_capacity = capacity;
|
||||
|
||||
_items.AddRange(LoadFromDiskSafe());
|
||||
TrimNoLock();
|
||||
}
|
||||
|
||||
public IReadOnlyList<HistoryItem> HistoryItems
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return [.. _items];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(HistoryItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_items.Add(item);
|
||||
_ = TrimNoLock();
|
||||
SaveNoLock();
|
||||
}
|
||||
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SetCapacity(int capacity)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegative(capacity);
|
||||
|
||||
bool trimmed;
|
||||
lock (_lock)
|
||||
{
|
||||
_capacity = capacity;
|
||||
trimmed = TrimNoLock();
|
||||
if (trimmed)
|
||||
{
|
||||
SaveNoLock();
|
||||
}
|
||||
}
|
||||
|
||||
if (trimmed)
|
||||
{
|
||||
Changed?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TrimNoLock()
|
||||
{
|
||||
var max = _capacity;
|
||||
if (_items.Count > max)
|
||||
{
|
||||
_items.RemoveRange(0, _items.Count - max);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<HistoryItem> LoadFromDiskSafe()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_filePath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var fileContent = File.ReadAllText(_filePath);
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||
return historyItems;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Unable to load history", ex);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveNoLock()
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_items, WebSearchJsonSerializationContext.Default.ListHistoryItem);
|
||||
File.WriteAllText(_filePath, json);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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 Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -9,11 +10,13 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
public interface ISettingsInterface
|
||||
{
|
||||
event EventHandler? HistoryChanged;
|
||||
|
||||
public bool GlobalIfURI { get; }
|
||||
|
||||
public uint HistoryItemCount { get; }
|
||||
public int HistoryItemCount { get; }
|
||||
|
||||
public List<ListItem> LoadHistory();
|
||||
public IReadOnlyList<HistoryItem> HistoryItems { get; }
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem);
|
||||
public void AddHistoryItem(HistoryItem historyItem);
|
||||
}
|
||||
|
||||
@@ -4,11 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -17,10 +14,16 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
private const string HistoryItemCountLegacySettingsKey = "ShowHistory";
|
||||
private readonly string _historyPath;
|
||||
|
||||
private static readonly string _namespace = "websearch";
|
||||
|
||||
public event EventHandler? HistoryChanged
|
||||
{
|
||||
add => _history.Changed += value;
|
||||
remove => _history.Changed -= value;
|
||||
}
|
||||
|
||||
private readonly HistoryStore _history;
|
||||
|
||||
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
|
||||
|
||||
private static readonly List<ChoiceSetSetting.Choice> _choices =
|
||||
@@ -46,9 +49,26 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
|
||||
public bool GlobalIfURI => _globalIfURI.Value;
|
||||
|
||||
public uint HistoryItemCount => uint.TryParse(_historyItemCount.Value, out var value) ? value : 0;
|
||||
public int HistoryItemCount => int.TryParse(_historyItemCount.Value, out var value) && value >= 0 ? value : 0;
|
||||
|
||||
internal static string SettingsJsonPath()
|
||||
public IReadOnlyList<HistoryItem> HistoryItems => _history.HistoryItems;
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
|
||||
Settings.Add(_globalIfURI);
|
||||
Settings.Add(_historyItemCount);
|
||||
|
||||
LoadSettings();
|
||||
|
||||
// Initialize history store after loading settings to get the correct capacity
|
||||
_history = new HistoryStore(HistoryStateJsonPath(), HistoryItemCount);
|
||||
|
||||
Settings.SettingsChanged += (_, _) => SaveSettings();
|
||||
}
|
||||
|
||||
private static string SettingsJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
@@ -57,7 +77,7 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
return Path.Combine(directory, "settings.json");
|
||||
}
|
||||
|
||||
internal static string HistoryStateJsonPath()
|
||||
private static string HistoryStateJsonPath()
|
||||
{
|
||||
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
|
||||
Directory.CreateDirectory(directory);
|
||||
@@ -66,156 +86,30 @@ public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
return Path.Combine(directory, "websearch_history.json");
|
||||
}
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem)
|
||||
public void AddHistoryItem(HistoryItem historyItem)
|
||||
{
|
||||
if (historyItem is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
List<HistoryItem> historyItems;
|
||||
|
||||
// Check if the file exists and load existing history
|
||||
if (File.Exists(_historyPath))
|
||||
{
|
||||
var existingContent = File.ReadAllText(_historyPath);
|
||||
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||
}
|
||||
else
|
||||
{
|
||||
historyItems = [];
|
||||
}
|
||||
|
||||
// Add the new history item
|
||||
historyItems.Add(historyItem);
|
||||
|
||||
// Determine the maximum number of items to keep based on HistoryItemCount
|
||||
if (HistoryItemCount > 0)
|
||||
{
|
||||
// Keep only the most recent `maxHistoryItems` items
|
||||
while (historyItems.Count > HistoryItemCount)
|
||||
{
|
||||
historyItems.RemoveAt(0); // Remove the oldest item
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize the updated list back to JSON and save it
|
||||
var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
|
||||
File.WriteAllText(_historyPath, historyJson);
|
||||
_history.Add(historyItem);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to add item to the search history", ex);
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||
}
|
||||
}
|
||||
|
||||
public List<ListItem> LoadHistory()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_historyPath))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// Read and deserialize JSON into a list of HistoryItem objects
|
||||
var fileContent = File.ReadAllText(_historyPath);
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||
|
||||
// Convert each HistoryItem to a ListItem
|
||||
var listItems = new List<ListItem>();
|
||||
foreach (var historyItem in historyItems)
|
||||
{
|
||||
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
|
||||
{
|
||||
Title = historyItem.SearchString,
|
||||
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture), // Ensures consistent formatting
|
||||
});
|
||||
}
|
||||
|
||||
listItems.Reverse();
|
||||
return listItems;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public SettingsManager()
|
||||
{
|
||||
FilePath = SettingsJsonPath();
|
||||
_historyPath = HistoryStateJsonPath();
|
||||
|
||||
Settings.Add(_globalIfURI);
|
||||
Settings.Add(_historyItemCount);
|
||||
|
||||
// Load settings from file upon initialization
|
||||
LoadSettings();
|
||||
|
||||
Settings.SettingsChanged += (s, a) => this.SaveSettings();
|
||||
}
|
||||
|
||||
private void ClearHistory()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_historyPath))
|
||||
{
|
||||
// Delete the history file
|
||||
File.Delete(_historyPath);
|
||||
|
||||
// Log that the history was successfully cleared
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "History cleared successfully." });
|
||||
}
|
||||
else
|
||||
{
|
||||
// Log that there was no history file to delete
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = "No history file found to clear." });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log any exception that occurs
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = $"Failed to clear history: {ex}" });
|
||||
}
|
||||
}
|
||||
|
||||
public override void SaveSettings()
|
||||
{
|
||||
base.SaveSettings();
|
||||
|
||||
try
|
||||
{
|
||||
if (HistoryItemCount == 0)
|
||||
{
|
||||
ClearHistory();
|
||||
}
|
||||
else if (HistoryItemCount > 0)
|
||||
{
|
||||
// Trim the history file if there are more items than the new limit
|
||||
if (File.Exists(_historyPath))
|
||||
{
|
||||
var existingContent = File.ReadAllText(_historyPath);
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||
|
||||
// Check if trimming is needed
|
||||
if (historyItems.Count > HistoryItemCount)
|
||||
{
|
||||
// Trim the list to keep only the most recent `HistoryItemCount` items
|
||||
historyItems = historyItems.Skip((int)(historyItems.Count - HistoryItemCount)).ToList();
|
||||
|
||||
// Save the trimmed history back to the file
|
||||
var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
|
||||
File.WriteAllText(_historyPath, trimmedHistoryJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
_history.SetCapacity(HistoryItemCount);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Failed to save the search history", ex);
|
||||
ExtensionHost.LogMessage(new LogMessage() { Message = ex.ToString() });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
@@ -16,31 +16,30 @@ using BrowserInfo = Microsoft.CmdPal.Ext.WebSearch.Helpers.DefaultBrowserInfo;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
|
||||
internal sealed partial class WebSearchListPage : DynamicListPage
|
||||
internal sealed partial class WebSearchListPage : DynamicListPage, IDisposable
|
||||
{
|
||||
private readonly string _iconPath = string.Empty;
|
||||
private readonly List<ListItem>? _historyItems;
|
||||
private readonly IconInfo _newSearchIcon = new(string.Empty);
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private readonly Lock _sync = new();
|
||||
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private List<ListItem> _allItems;
|
||||
private IListItem[] _allItems = [];
|
||||
private List<ListItem> _historyItems = [];
|
||||
|
||||
public WebSearchListPage(ISettingsInterface settingsManager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(settingsManager);
|
||||
|
||||
Name = Resources.command_item_title;
|
||||
Title = Resources.command_item_title;
|
||||
Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png");
|
||||
_allItems = [];
|
||||
Id = "com.microsoft.cmdpal.websearch";
|
||||
|
||||
_settingsManager = settingsManager;
|
||||
_historyItems = _settingsManager.HistoryItemCount != 0 ? _settingsManager.LoadHistory() : null;
|
||||
if (_historyItems is not null)
|
||||
{
|
||||
_allItems.AddRange(_historyItems);
|
||||
}
|
||||
_settingsManager.HistoryChanged += SettingsManagerOnHistoryChanged;
|
||||
|
||||
// It just looks viewer to have string twice on the page, and default placeholder is good enough
|
||||
PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty;
|
||||
PlaceholderText = _allItems.Length > 0 ? Resources.plugin_description : string.Empty;
|
||||
|
||||
EmptyContent = new CommandItem(new NoOpCommand())
|
||||
{
|
||||
@@ -48,45 +47,102 @@ internal sealed partial class WebSearchListPage : DynamicListPage
|
||||
Title = Properties.Resources.plugin_description,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
};
|
||||
|
||||
UpdateHistory();
|
||||
RequeryAndUpdateItems(SearchText);
|
||||
}
|
||||
|
||||
public List<ListItem> Query(string query)
|
||||
private void SettingsManagerOnHistoryChanged(object? sender, EventArgs e)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
IEnumerable<ListItem>? filteredHistoryItems = null;
|
||||
UpdateHistory();
|
||||
RequeryAndUpdateItems(SearchText);
|
||||
}
|
||||
|
||||
if (_historyItems is not null)
|
||||
private void UpdateHistory()
|
||||
{
|
||||
List<ListItem> history = [];
|
||||
|
||||
if (_settingsManager.HistoryItemCount > 0)
|
||||
{
|
||||
filteredHistoryItems = _settingsManager.HistoryItemCount != 0 ? ListHelpers.FilterList(_historyItems, query).OfType<ListItem>() : null;
|
||||
var items = _settingsManager.HistoryItems;
|
||||
for (var index = items.Count - 1; index >= 0; index--)
|
||||
{
|
||||
var historyItem = items[index];
|
||||
history.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, _settingsManager))
|
||||
{
|
||||
Title = historyItem.SearchString,
|
||||
Subtitle = historyItem.Timestamp.ToString("g", CultureInfo.InvariantCulture),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<ListItem>();
|
||||
lock (_sync)
|
||||
{
|
||||
_historyItems = history;
|
||||
}
|
||||
}
|
||||
|
||||
private static IListItem[] Query(string query, List<ListItem> historySnapshot, ISettingsInterface settingsManager, IconInfo newSearchIcon)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(query);
|
||||
|
||||
var filteredHistoryItems = settingsManager.HistoryItemCount > 0
|
||||
? ListHelpers.FilterList(historySnapshot, query)
|
||||
: [];
|
||||
|
||||
var results = new List<IListItem>();
|
||||
|
||||
if (!string.IsNullOrEmpty(query))
|
||||
{
|
||||
var searchTerm = query;
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager))
|
||||
var result = new ListItem(new SearchWebCommand(searchTerm, settingsManager))
|
||||
{
|
||||
Title = searchTerm,
|
||||
Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName),
|
||||
Icon = new IconInfo(_iconPath),
|
||||
Icon = newSearchIcon,
|
||||
};
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
if (filteredHistoryItems is not null)
|
||||
results.AddRange(filteredHistoryItems);
|
||||
|
||||
return [.. results];
|
||||
}
|
||||
|
||||
private void RequeryAndUpdateItems(string search)
|
||||
{
|
||||
List<ListItem> historySnapshot;
|
||||
lock (_sync)
|
||||
{
|
||||
results.AddRange(filteredHistoryItems);
|
||||
historySnapshot = _historyItems;
|
||||
}
|
||||
|
||||
return results;
|
||||
var items = Query(search ?? string.Empty, historySnapshot, _settingsManager, _newSearchIcon);
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_allItems = items;
|
||||
}
|
||||
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch)
|
||||
{
|
||||
_allItems = [.. Query(newSearch)];
|
||||
RaiseItemsChanged(0);
|
||||
RequeryAndUpdateItems(newSearch);
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => [.. _allItems];
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _allItems;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_settingsManager.HistoryChanged -= SettingsManagerOnHistoryChanged;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Properties;
|
||||
@@ -15,6 +16,9 @@ public partial class WebSearchCommandsProvider : CommandProvider
|
||||
private readonly SettingsManager _settingsManager = new();
|
||||
private readonly FallbackExecuteSearchItem _fallbackItem;
|
||||
private readonly FallbackOpenURLItem _openUrlFallbackItem;
|
||||
private readonly WebSearchTopLevelCommandItem _webSearchTopLevelItem;
|
||||
private readonly ICommandItem[] _topLevelItems;
|
||||
private readonly IFallbackCommandItem[] _fallbackCommands;
|
||||
|
||||
public WebSearchCommandsProvider()
|
||||
{
|
||||
@@ -25,18 +29,27 @@ public partial class WebSearchCommandsProvider : CommandProvider
|
||||
|
||||
_fallbackItem = new FallbackExecuteSearchItem(_settingsManager);
|
||||
_openUrlFallbackItem = new FallbackOpenURLItem(_settingsManager);
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands()
|
||||
{
|
||||
return [new WebSearchTopLevelCommandItem(_settingsManager)
|
||||
_webSearchTopLevelItem = new WebSearchTopLevelCommandItem(_settingsManager)
|
||||
{
|
||||
MoreCommands = [
|
||||
MoreCommands =
|
||||
[
|
||||
new CommandContextItem(Settings!.SettingsPage),
|
||||
],
|
||||
}
|
||||
];
|
||||
};
|
||||
_topLevelItems = [_webSearchTopLevelItem];
|
||||
_fallbackCommands = [_openUrlFallbackItem, _fallbackItem];
|
||||
}
|
||||
|
||||
public override IFallbackCommandItem[]? FallbackCommands() => [_openUrlFallbackItem, _fallbackItem];
|
||||
public override ICommandItem[] TopLevelCommands() => _topLevelItems;
|
||||
|
||||
public override IFallbackCommandItem[]? FallbackCommands() => _fallbackCommands;
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
_webSearchTopLevelItem?.Dispose();
|
||||
|
||||
base.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
using Microsoft.CmdPal.Ext.WebSearch.Pages;
|
||||
@@ -13,7 +12,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch;
|
||||
|
||||
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler
|
||||
public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandler, IDisposable
|
||||
{
|
||||
private readonly SettingsManager _settingsManager;
|
||||
|
||||
@@ -27,17 +26,29 @@ public partial class WebSearchTopLevelCommandItem : CommandItem, IFallbackHandle
|
||||
|
||||
private void SetDefaultTitle() => Title = Resources.command_item_title;
|
||||
|
||||
private void ReplaceCommand(ICommand newCommand)
|
||||
{
|
||||
(Command as IDisposable)?.Dispose();
|
||||
Command = newCommand;
|
||||
}
|
||||
|
||||
public void UpdateQuery(string query)
|
||||
{
|
||||
if (string.IsNullOrEmpty(query))
|
||||
{
|
||||
SetDefaultTitle();
|
||||
Command = new WebSearchListPage(_settingsManager);
|
||||
ReplaceCommand(new WebSearchListPage(_settingsManager));
|
||||
}
|
||||
else
|
||||
{
|
||||
Title = query;
|
||||
Command = new SearchWebCommand(query, _settingsManager);
|
||||
ReplaceCommand(new SearchWebCommand(query, _settingsManager));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
(Command as IDisposable)?.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
// 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.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace SamplePagesExtension.Pages;
|
||||
|
||||
internal sealed partial class SampleIconPage : ListPage
|
||||
{
|
||||
private readonly IListItem[] _items =
|
||||
[
|
||||
/*
|
||||
* Quick intro to Unicode in source code:
|
||||
* - Every character has a code point (e.g., U+0041 = 'A').
|
||||
* - Code points up to U+FFFF use \u1234 (4 hex digits and lowercase u).
|
||||
* - Code points above that (up to U+10FFFF) use \U12345678 (8 hex digits and capital letter U).
|
||||
* - If your source file is UTF-8, you can type the character directly, but it may not display properly in editors,
|
||||
* and it's harder to see the actual code point.
|
||||
* - Some symbols (like many emojis) are built from multiple code points
|
||||
* joined together (e.g., 👋🏻 = U+1F44B + U+1F3FB).
|
||||
*
|
||||
* Examples:
|
||||
* 😍 = "😍" or "\U0001F60D"
|
||||
* 👋🏻 = "👋🏻" or "\U0001F44B\U0001F3FB"
|
||||
* 🧙♂️ = "🧙♂️" or "\U0001F9D9\u200D\u2642\U0000FE0F" (male mage)
|
||||
* 🧙🏿♀️ = "🧙🏿♀️" or "\U0001F9D9\U0001F3FF\u200D\u2640\U0000FE0F" (dark-skinned woman mage)
|
||||
*
|
||||
*/
|
||||
|
||||
// Emoji Smiling Face with Heart-Eyes
|
||||
// Unicode: \U0001F60D
|
||||
BuildIconItem("😍", "Standard emoji icon", "Basic emoji character rendered as an icon"),
|
||||
|
||||
// Emoji Smiling Face with Heart-Eyes
|
||||
// Unicode: \U0001F60D\U0001F643\U0001F622
|
||||
BuildIconItem("😍🙃😢", "Multiple emojis", "Use of multiple emojis for icon is not allowed"),
|
||||
|
||||
// Emoji Smiling Face with Sunglasses
|
||||
// Unicode: \U0001F60E
|
||||
BuildIconItem("\U0001F60E", "Unicode escape sequence emoji", "Emoji defined using Unicode escape sequence notation"),
|
||||
|
||||
// Segoe Fluent Icons font icon
|
||||
// Unicode: \uE8D4
|
||||
BuildIconItem("\uE8D4", "Segoe Fluent icon demonstration", "Segoe Fluent/MDL2 icon from system font\nWorks as an icon but won't display properly in button text"),
|
||||
|
||||
// Extended pictographic symbol for keyboard
|
||||
BuildIconItem("\u2328", "Extended pictographic symbol", "Pictographic symbol representing a keyboard"),
|
||||
|
||||
// Capital letter A
|
||||
BuildIconItem("A", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||
|
||||
// Letter 1
|
||||
// Unicode: \U00000031
|
||||
BuildIconItem("1", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||
|
||||
// Emoji Keycap Digit Two ... 2️⃣
|
||||
// Unicode: \U00000032\U000020E3
|
||||
// This is a sequence of three code points: the digit '2' (U+0032), and a combining enclosing keycap (U+20E3). No variation selector is used here.
|
||||
BuildIconItem("\U00000032\U000020E3", "Emoji without variation selector", "Emoji character doesn't have VS16 variation selector to render as text"),
|
||||
|
||||
// Emoji Keycap Digit Three ... 3️⃣
|
||||
// Unicode: \U00000033\U0000FE0F\U000020E3
|
||||
// This is a sequence of three code points: the digit '3' (U+0033), a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
|
||||
BuildIconItem("3️⃣", "Emoji with variation selector", "Emoji character using a variation selector to specify emoji presentation"),
|
||||
|
||||
// Symbol #
|
||||
// Unicode: \u0023
|
||||
BuildIconItem("#", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||
|
||||
// Symbol # keycap
|
||||
// Unicode: \u0023\ufe0f\u20e3
|
||||
// Sequence of 3 code points: symbol #, a variation selector (U+FE0F) to specify emoji presentation, and a combining enclosing keycap (U+20E3).
|
||||
BuildIconItem("\u0023\ufe0f\u20e3", "Simple text character as icon", "Basic letter character used as an icon demonstration"),
|
||||
|
||||
// Capital letter WM
|
||||
// This is two characters, which is not a valid icon representation. It will be replaced by a placeholder signalizing an invalid icon.
|
||||
BuildIconItem("WM", "Invalid icon representation", "String with multiple characters that does not correspond to a valid single icon"),
|
||||
|
||||
// Emoji Mage
|
||||
// Unicode: \U0001F9D9
|
||||
BuildIconItem("🧙", "Single code-point emoji example", "Simple emoji character using a single Unicode code point"),
|
||||
|
||||
// Emoji Male Mage (Mage with gender modifier)
|
||||
// Unicode: \U0001F9D9\u200D\u2642\uFE0F
|
||||
BuildIconItem("🧙♂️", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for male variant"),
|
||||
|
||||
// Emoji Woman Mage (Mage with gender modifier)
|
||||
// Unicode: \U0001F9D9\u200D\u2640\uFE0F
|
||||
BuildIconItem("\U0001F9D9\u200D\u2640\uFE0F", "Complex emoji with gender modifier", "Composite emoji using Zero-Width Joiner (ZWJ) sequence for female variant"),
|
||||
|
||||
// Emoji Waving Hand
|
||||
// Unicode: \U0001F44B
|
||||
BuildIconItem("👋", "Basic hand gesture emoji", "Standard emoji character representing a waving hand"),
|
||||
|
||||
// Emoji Waving Hand + Light Skin Tone
|
||||
// Unicode: \U0001F44B\U0001F3FB
|
||||
BuildIconItem("👋🏻", "Emoji with light skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (light)"),
|
||||
|
||||
// Emoji Waving Hand + Dark Skin Tone
|
||||
// Unicode: \U0001F44B\U0001F3FF
|
||||
BuildIconItem("\U0001F44B\U0001F3FF", "Emoji with dark skin tone modifier", "Emoji enhanced with Unicode skin tone modifier (dark)"),
|
||||
|
||||
// Flag of Czechia (Czech Republic)
|
||||
// Unicode: \U0001F1E8\U0001F1FF
|
||||
BuildIconItem("\U0001F1E8\U0001F1FF", "Flag emoji using regional indicators", "Emoji flag constructed from regional indicator symbols for Czechia"),
|
||||
|
||||
// Use of ZWJ without emojis
|
||||
// KA (\u0995) + VIRAMA (\u09CD) + ZWJ (\u200D) - shows the half-form KA
|
||||
// Unicode: \u0995\u09CD\u200D
|
||||
BuildIconItem("\u0995\u09CD\u200D", "Use of ZWJ in non-emoji context", "Shows the half-form KA"),
|
||||
|
||||
// Use of ZWJ without emojis
|
||||
// KA (\u0995) + VIRAMA (\u09CD) + Shows full KA with an explicit virama mark (not half-form).
|
||||
// Unicode: \u0995\u09CD
|
||||
BuildIconItem("\u0995\u09CD", "Use of ZWJ in non-emoji context", "Shows full KA with an explicit virama mark"),
|
||||
|
||||
// mahjong tile red dragon (using Unicode escape sequence)
|
||||
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
|
||||
// Unicode: \U0001F004
|
||||
BuildIconItem("\U0001F004", "Mahjong tile emoji (red dragon)", "Mahjong tile red dragon emoji character using Unicode escape sequence"),
|
||||
|
||||
// mahjong tile green dragon (non-emoji)
|
||||
// https://en.wikipedia.org/wiki/Mahjong_Tiles_(Unicode_block)
|
||||
// Unicode: \U0001F005
|
||||
BuildIconItem("\U0001F005", "Mahjong tile non-emoji (green dragon)", "Mahjong tile character that is not classified as an emoji"),
|
||||
|
||||
// Play, PlayPause, Stop
|
||||
BuildIconItem("\u25B6", "Play symbol (standalone)", "Play symbol"),
|
||||
BuildIconItem("\u25B6\uFE0E", "Play symbol + VS15 (request text)", "Play symbol with variation specifier requesting rendering as text"),
|
||||
BuildIconItem("\u25B6\uFE0F", "Play symbol + VS16 (request emoji)", "Play symbol with variation specifier requesting rendering as emoji "),
|
||||
BuildIconItem("⏯️", "Play/Pause keycap emoji", "Play/Pause keycap emoji doesn't have plain text variant"),
|
||||
BuildIconItem("⏸️", "Pause keycap emoji", "Pause keycap emoji doesn't have plain text variant"),
|
||||
|
||||
// Copyright and emoji copyright:
|
||||
BuildIconItem("\u00a9", "Copyright symbol (standalone)", "Copyright symbol that is not classified as an emoji"),
|
||||
BuildIconItem("\u00a9\uFE0E", "Copyright symbol + VS15 (request text)", "Copyright symbol that is not classified as an emoji"),
|
||||
BuildIconItem("\u00a9\uFE0F", "Copyright symbol + VS16 (request emoji)", "Copyright symbol that is not classified as an emoji"),
|
||||
|
||||
// Tag flags
|
||||
BuildIconItem("🏳️", "White Flag", "White Flag"),
|
||||
BuildIconItem("\U0001F3F4\u200D\u2620\uFE0F", "Pirate Flag", "Pirate Flag"),
|
||||
];
|
||||
|
||||
public SampleIconPage()
|
||||
{
|
||||
Icon = new IconInfo("\uE8BA");
|
||||
Name = "Sample Icon Page";
|
||||
ShowDetails = true;
|
||||
}
|
||||
|
||||
public override IListItem[] GetItems() => _items;
|
||||
|
||||
private static ListItem BuildIconItem(string icon, string title, string description)
|
||||
{
|
||||
var iconInfo = new IconInfo(icon);
|
||||
|
||||
return new ListItem(new CopyTextCommand(icon) { Name = "Action with " + icon })
|
||||
{
|
||||
Title = title,
|
||||
Subtitle = description,
|
||||
Icon = iconInfo,
|
||||
Tags = [
|
||||
new Tag("Tag") { Icon = iconInfo },
|
||||
],
|
||||
Details = new Details
|
||||
{
|
||||
HeroImage = iconInfo,
|
||||
Title = title,
|
||||
Body = description,
|
||||
Metadata = [
|
||||
new DetailsElement
|
||||
{
|
||||
Key = "Unicode Code Points",
|
||||
Data = new DetailsTags
|
||||
{
|
||||
Tags = icon.EnumerateRunes()
|
||||
.Select(rune => rune.Value <= 0xFFFF ? $"\\u{rune.Value:X4}" : $"\\U{rune.Value:X8}")
|
||||
.Select(t => new Tag(t))
|
||||
.ToArray<ITag>(),
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using SamplePagesExtension.Pages;
|
||||
|
||||
namespace SamplePagesExtension;
|
||||
|
||||
@@ -37,6 +38,11 @@ public partial class SamplesListPage : ListPage
|
||||
Title = "Demo of OnLoad/OnUnload",
|
||||
Subtitle = "Changes the list of items every time the page is opened / closed",
|
||||
},
|
||||
new ListItem(new SampleIconPage())
|
||||
{
|
||||
Title = "Sample Icon Page",
|
||||
Subtitle = "A demo of using icons in various ways",
|
||||
},
|
||||
|
||||
// Content pages
|
||||
new ListItem(new SampleContentPage())
|
||||
|
||||
Reference in New Issue
Block a user