Compare commits

...

12 Commits

Author SHA1 Message Date
Mike Griese
d59849b0f6 good nits 2025-04-17 06:55:44 -05:00
Mike Griese
a5090ea63b Merge remote-tracking branch 'origin/main' into dev/migrie/f/winget-nits 2025-04-17 06:41:17 -05:00
Mike Griese
05218e8af6 Add the list item requested shortcuts back (#38573)
* [x] Re-adds the context menu shortcut text
* [x] Hooks up the keybindings to the search box so that you can just press the keys while you have an item selected, and do a context command
* [x] Hook these keybindings up to the context flyout itself
* [x] Adds a sample for testing

Solves #38271
2025-04-17 06:13:11 -05:00
Yu Leng
6cf73ce839 [cmdpal] Port v1 calculator extension (#38629)
* init

* update

* Remove duplicated cp command

* Change the long desc

* Update notice.md

* Use the same icon for fallback item

* Add Rappl to expect list

* update notice.md

* Move the original order back.

* Make Radians become default choice

* Fix empty result

* Remove unused settings.
Move history back.
Refactory the query logic

* fix typo

* merge main

* CmdPal: minor calc updates (#38914)

A bunch of calc updates

* maintain the visibility of the history
* add other formats to the context menu #38708
* some other icon tidying

---------

Co-authored-by: Yu Leng (from Dev Box) <yuleng@microsoft.com>
Co-authored-by: Mike Griese <migrie@microsoft.com>
2025-04-17 14:59:10 +08:00
Mike Griese
9d5928ace0 straggler? 2025-04-15 06:25:17 -05:00
Mike Griese
7e3b3a8087 Merge remote-tracking branch 'origin/main' into dev/migrie/f/winget-nits 2025-04-15 06:24:29 -05:00
Mike Griese
b42b7a384e Merge remote-tracking branch 'origin/main' into dev/migrie/f/winget-nits 2025-04-09 10:15:43 -05:00
Mike Griese
55a2db31cb kinda seems like this file could be generated by the build, since it's _generated by the build_ 2025-04-03 06:51:02 -05:00
Mike Griese
4b7eae662b Merge remote-tracking branch 'origin/main' into dev/migrie/f/winget-nits 2025-04-02 14:20:09 -05:00
Mike Griese
658c1b5006 Revert "Use the InProcCOM winget API"
This reverts commit 709b5dc6d4.
2025-03-26 10:24:33 -05:00
Mike Griese
709b5dc6d4 Use the InProcCOM winget API
Unsurprisingly, this didn't work
2025-03-26 10:24:05 -05:00
Mike Griese
f5a08cc2f0 A bunch of winget nits 2025-03-26 10:01:39 -05:00
38 changed files with 1884 additions and 181 deletions

View File

@@ -1299,6 +1299,7 @@ QUNS
QXZ
RAII
RAlt
Rappl
randi
Rasterization
Rasterize

View File

@@ -45,7 +45,7 @@
<PackageVersion Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<!-- Package Microsoft.Win32.SystemEvents added as a hack for being able to exclude the runtime assets so they don't conflict with 8.0.1. This is a dependency of System.Drawing.Common but the 8.0.1 version wasn't published to nuget. -->
<PackageVersion Include="Microsoft.Win32.SystemEvents" Version="9.0.4" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.120-preview" />
<PackageVersion Include="Microsoft.WindowsPackageManager.ComInterop" Version="1.10.340" />
<PackageVersion Include="Microsoft.Windows.Compatibility" Version="9.0.4" />
<PackageVersion Include="Microsoft.Windows.CsWin32" Version="0.2.46-beta" />
<!-- CsWinRT version needs to be set to have a WinRT.Runtime.dll at the same version contained inside the NET SDK we're currently building on CI. -->

View File

@@ -75,6 +75,40 @@ OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
```
## Utility: Command Palette Built-in Extensions
### Calculator
#### Mages
We use the Mages NuGet package for calculating the result of expression.
**Source**: [https://github.com/FlorianRappl/Mages](https://github.com/FlorianRappl/Mages)
```
The MIT License (MIT)
Copyright (c) 2016 - 2025 Florian Rappl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
## Utility: File Explorer Add-ins
### Monaco Editor
@@ -1439,7 +1473,7 @@ SOFTWARE.
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.WindowsAppSDK 1.6.250205002
- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4

View File

@@ -7,11 +7,15 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
IRecipient<UpdateCommandBarMessage>
IRecipient<UpdateCommandBarMessage>,
IRecipient<UpdateItemKeybindingsMessage>
{
public ICommandBarContext? SelectedItem
{
@@ -49,13 +53,18 @@ public partial class CommandBarViewModel : ObservableObject,
[ObservableProperty]
public partial ObservableCollection<CommandContextItemViewModel> ContextCommands { get; set; } = [];
private Dictionary<KeyChord, CommandContextItemViewModel>? _contextKeybindings;
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register<UpdateCommandBarMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
@@ -131,4 +140,22 @@ public partial class CommandBarViewModel : ObservableObject,
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
}
}
public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
{
if (_contextKeybindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
return true;
}
}
return false;
}
}

View File

@@ -9,12 +9,16 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandContextItemViewModel(ICommandContextItem contextItem, WeakReference<IPageContext> context) : CommandItemViewModel(new(contextItem), context)
{
private readonly KeyChord nullKeyChord = new(0, 0, 0);
public new ExtensionObject<ICommandContextItem> Model { get; } = new(contextItem);
public bool IsCritical { get; private set; }
public KeyChord? RequestedShortcut { get; private set; }
public bool HasRequestedShortcut => RequestedShortcut != null && (RequestedShortcut.Value != nullKeyChord);
public override void InitializeProperties()
{
if (IsInitialized)
@@ -31,6 +35,9 @@ public partial class CommandContextItemViewModel(ICommandContextItem contextItem
}
IsCritical = contextItem.IsCritical;
// I actually don't think this will ever actually be null, because
// KeyChord is a struct, which isn't nullable in WinRT
if (contextItem.RequestedShortcut != null)
{
RequestedShortcut = new(

View File

@@ -398,6 +398,23 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
/// <summary>
/// Generates a mapping of key -> command item for this particular item's
/// MoreCommands. (This won't include the primary Command, but it will
/// include the secondary one). This map can be used to quickly check if a
/// shortcut key was pressed
/// </summary>
/// <returns>a dictionary of KeyChord -> Context commands, for all commands
/// that have a shortcut key set.</returns>
internal Dictionary<KeyChord, CommandContextItemViewModel> Keybindings()
{
return MoreCommands
.Where(c => c.HasRequestedShortcut)
.ToDictionary(
c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
c => c);
}
}
[Flags]

View File

@@ -344,6 +344,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(item));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(item.Keybindings()));
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send<ShowDetailsMessage>(new(item.Details));

View File

@@ -51,6 +51,12 @@ public record PerformCommandMessage
Context = context.Unsafe;
}
public PerformCommandMessage(CommandContextItemViewModel contextCommand)
{
Command = contextCommand.Command.Model;
Context = contextCommand.Model.Unsafe;
}
public PerformCommandMessage(ConfirmResultViewModel vm)
{
Command = vm.PrimaryCommand.Model;

View File

@@ -0,0 +1,9 @@
// 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;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
public record UpdateItemKeybindingsMessage(Dictionary<KeyChord, CommandContextItemViewModel>? Keys);

View File

@@ -53,14 +53,14 @@
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Title, Mode=OneWay}" />
<!--<TextBlock
<TextBlock
Grid.Column="2"
Margin="16,0,0,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Foreground="{ThemeResource MenuFlyoutItemKeyboardAcceleratorTextForeground}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />-->
Text="{x:Bind RequestedShortcut, Mode=OneWay, Converter={StaticResource KeyChordToStringConverter}}" />
</Grid>
</DataTemplate>
@@ -263,6 +263,7 @@
ItemClick="CommandsDropdown_ItemClick"
ItemTemplate="{StaticResource ContextMenuViewModelTemplate}"
ItemsSource="{x:Bind ViewModel.ContextCommands, Mode=OneWay}"
KeyDown="CommandsDropdown_KeyDown"
SelectionMode="None">
<ListView.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem">

View File

@@ -6,10 +6,13 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Input;
using Windows.System;
using Windows.UI.Core;
namespace Microsoft.CmdPal.UI.Controls;
@@ -89,4 +92,23 @@ public sealed partial class CommandBar : UserControl,
MoreCommandsButton.Flyout.Hide();
}
}
private void CommandsDropdown_KeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Handled)
{
return;
}
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
{
e.Handled = true;
}
}
}

View File

@@ -8,6 +8,8 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -21,6 +23,7 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient<GoHomeMessage>,
IRecipient<FocusSearchBoxMessage>,
IRecipient<UpdateItemKeybindingsMessage>,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -31,6 +34,8 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
private Dictionary<KeyChord, CommandContextItemViewModel>? _keyBindings;
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -69,6 +74,7 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register<GoHomeMessage>(this);
WeakReferenceMessenger.Default.Register<FocusSearchBoxMessage>(this);
WeakReferenceMessenger.Default.Register<UpdateItemKeybindingsMessage>(this);
}
public void ClearSearch()
@@ -105,7 +111,9 @@ public sealed partial class SearchBar : UserControl,
var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
if (ctrlPressed && e.Key == VirtualKey.Enter)
{
// ctrl+enter
@@ -164,6 +172,19 @@ public sealed partial class SearchBar : UserControl,
{
WeakReferenceMessenger.Default.Send<NavigateBackMessage>(new());
}
if (_keyBindings != null)
{
// Does the pressed key match any of the keybindings?
var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
{
// TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
// so that the correct item is activated.
WeakReferenceMessenger.Default.Send<PerformCommandMessage>(new(item));
e.Handled = true;
}
}
}
private void FilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
@@ -282,4 +303,9 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
public void Receive(UpdateItemKeybindingsMessage message)
{
_keyBindings = message.Keys;
}
}

View File

@@ -187,6 +187,8 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
WeakReferenceMessenger.Default.Send<UpdateItemKeybindingsMessage>(new(null));
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
@@ -427,8 +429,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
}
_settingsWindow.Activate();
WeakReferenceMessenger.Default.Send<UpdateCommandBarMessage>(new(null));
});
}

View File

@@ -2,176 +2,34 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Text;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Pages;
using Microsoft.CmdPal.Ext.Calc.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc;
public partial class CalculatorCommandProvider : CommandProvider
{
private readonly ListItem _listItem = new(new CalculatorListPage()) { Subtitle = Resources.calculator_top_level_subtitle };
private readonly FallbackCalculatorItem _fallback = new();
private readonly ListItem _listItem = new(new CalculatorListPage(settings))
{
Subtitle = Resources.calculator_top_level_subtitle,
MoreCommands = [new CommandContextItem(settings.Settings.SettingsPage)],
};
private readonly FallbackCalculatorItem _fallback = new(settings);
private static SettingsManager settings = new();
public CalculatorCommandProvider()
{
Id = "Calculator";
DisplayName = Resources.calculator_display_name;
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
Icon = CalculatorIcons.ProviderIcon;
Settings = settings.Settings;
}
public override ICommandItem[] TopLevelCommands() => [_listItem];
public override IFallbackCommandItem[] FallbackCommands() => [_fallback];
}
// The calculator page is a dynamic list page
// * The first command is where we display the results. Title=result, Subtitle=query
// - The default command is `SaveCommand`.
// - When you save, insert into list at spot 1
// - change SearchText to the result
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
// * The rest of the items are previously saved results
// - Command is a CopyCommand
// - Each item also sets the TextToSuggest to the result
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
public sealed partial class CalculatorListPage : DynamicListPage
{
private readonly List<ListItem> _items = [];
private readonly SaveCommand _saveCommand = new();
private readonly CopyTextCommand _copyContextCommand;
private readonly CommandContextItem _copyContextMenuItem;
private static readonly CompositeFormat ErrorMessage = System.Text.CompositeFormat.Parse(Properties.Resources.calculator_error);
public CalculatorListPage()
{
Icon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
Name = Resources.calculator_title;
PlaceholderText = Resources.calculator_placeholder_text;
Id = "com.microsoft.cmdpal.calculator";
_copyContextCommand = new CopyTextCommand(string.Empty);
_copyContextMenuItem = new CommandContextItem(_copyContextCommand);
_items.Add(new(_saveCommand) { Icon = new IconInfo("\uE94E") });
UpdateSearchText(string.Empty, string.Empty);
_saveCommand.SaveRequested += HandleSave;
}
private void HandleSave(object sender, object args)
{
var lastResult = _items[0].Title;
if (!string.IsNullOrEmpty(lastResult))
{
var li = new ListItem(new CopyTextCommand(lastResult))
{
Title = _items[0].Title,
Subtitle = _items[0].Subtitle,
TextToSuggest = lastResult,
};
_items.Insert(1, li);
_items[0].Subtitle = string.Empty;
SearchText = lastResult;
this.RaiseItemsChanged(this._items.Count);
}
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
var firstItem = _items[0];
if (string.IsNullOrEmpty(newSearch))
{
firstItem.Title = Resources.calculator_placeholder_text;
firstItem.Subtitle = string.Empty;
firstItem.MoreCommands = [];
}
else
{
_copyContextCommand.Text = ParseQuery(newSearch, out var result) ? result : string.Empty;
firstItem.Title = result;
firstItem.Subtitle = newSearch;
firstItem.MoreCommands = [_copyContextMenuItem];
}
}
internal static bool ParseQuery(string equation, out string result)
{
try
{
var resultNumber = new DataTable().Compute(equation, null);
result = resultNumber.ToString() ?? string.Empty;
return true;
}
catch (Exception e)
{
result = string.Format(CultureInfo.CurrentCulture, ErrorMessage, e.Message);
return false;
}
}
public override IListItem[] GetItems() => _items.ToArray();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
public sealed partial class SaveCommand : InvokableCommand
{
public event TypedEventHandler<object, object> SaveRequested;
public SaveCommand()
{
Name = Resources.calculator_save_command_name;
}
public override ICommandResult Invoke()
{
SaveRequested?.Invoke(this, this);
return CommandResult.KeepOpen();
}
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "This is sample code")]
internal sealed partial class FallbackCalculatorItem : FallbackCommandItem
{
private readonly CopyTextCommand _copyCommand = new(string.Empty);
private static readonly IconInfo _cachedIcon = IconHelpers.FromRelativePath("Assets\\Calculator.svg");
public FallbackCalculatorItem()
: base(new NoOpCommand(), Resources.calculator_title)
{
Command = _copyCommand;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = Resources.calculator_placeholder_text;
Icon = _cachedIcon;
}
public override void UpdateQuery(string query)
{
if (CalculatorListPage.ParseQuery(query, out var result))
{
_copyCommand.Text = result;
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
Title = result;
// we have to make the subtitle the equation,
// so that we will still string match the original query
// Otherwise, something like 1+2 will have a title of "3" and not match
Subtitle = query;
}
else
{
_copyCommand.Text = string.Empty;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = string.Empty;
}
}
}

View File

@@ -0,0 +1,86 @@
// 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;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class BracketHelper
{
public static bool IsBracketComplete(string query)
{
if (string.IsNullOrWhiteSpace(query))
{
return true;
}
var valueTuples = query
.Select(BracketTrail)
.Where(r => r != default);
var trailTest = new Stack<TrailType>();
foreach (var (direction, type) in valueTuples)
{
switch (direction)
{
case TrailDirection.Open:
trailTest.Push(type);
break;
case TrailDirection.Close:
// Try to get item out of stack
if (!trailTest.TryPop(out var popped))
{
return false;
}
if (type != popped)
{
return false;
}
continue;
default:
{
throw new ArgumentOutOfRangeException($"Can't process value (Parameter direction: {direction})");
}
}
}
return trailTest.Count == 0;
}
private static (TrailDirection Direction, TrailType Type) BracketTrail(char @char)
{
switch (@char)
{
case '(':
return (TrailDirection.Open, TrailType.Round);
case ')':
return (TrailDirection.Close, TrailType.Round);
case '[':
return (TrailDirection.Open, TrailType.Bracket);
case ']':
return (TrailDirection.Close, TrailType.Bracket);
default:
return default;
}
}
private enum TrailDirection
{
None,
Open,
Close,
}
private enum TrailType
{
None,
Bracket,
Round,
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text.RegularExpressions;
using Mages.Core;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class CalculateEngine
{
private static readonly Engine _magesEngine = new Engine(new Configuration
{
Scope = new Dictionary<string, object>
{
{ "e", Math.E }, // e is not contained in the default mages engine
},
});
public const int RoundingDigits = 10;
public enum TrigMode
{
Radians,
Degrees,
Gradians,
}
/// <summary>
/// Interpret
/// </summary>
/// <param name="cultureInfo">Use CultureInfo.CurrentCulture if something is user facing</param>
public static CalculateResult Interpret(SettingsManager settings, string input, CultureInfo cultureInfo, out string error)
{
error = default;
if (!CalculateHelper.InputValid(input))
{
return default;
}
// check for division by zero
// We check if the string contains a slash followed by space (optional) and zero. Whereas the zero must not be followed by a dot, comma, 'b', 'o' or 'x' as these indicate a number with decimal digits or a binary/octal/hexadecimal value respectively. The zero must also not be followed by other digits.
if (new Regex("\\/\\s*0(?!(?:[,\\.0-9]|[box]0*[1-9a-f]))", RegexOptions.IgnoreCase).Match(input).Success)
{
error = Properties.Resources.calculator_division_by_zero;
return default;
}
// mages has quirky log representation
// mage has log == ln vs log10
input = input.
Replace("log(", "log10(", true, CultureInfo.CurrentCulture).
Replace("ln(", "log(", true, CultureInfo.CurrentCulture);
input = CalculateHelper.FixHumanMultiplicationExpressions(input);
// Get the user selected trigonometry unit
TrigMode trigMode = settings.TrigUnit;
// Modify trig functions depending on angle unit setting
input = CalculateHelper.UpdateTrigFunctions(input, trigMode);
// Expand conversions between trig units
input = CalculateHelper.ExpandTrigConversions(input, trigMode);
var result = _magesEngine.Interpret(input);
// This could happen for some incorrect queries, like pi(2)
if (result == null)
{
error = Properties.Resources.calculator_expression_not_complete;
return default;
}
result = TransformResult(result);
if (result is string)
{
error = result as string;
return default;
}
if (string.IsNullOrEmpty(result?.ToString()))
{
return default;
}
var decimalResult = Convert.ToDecimal(result, cultureInfo);
var roundedResult = Round(decimalResult);
return new CalculateResult()
{
Result = decimalResult,
RoundedResult = roundedResult,
};
}
public static decimal Round(decimal value)
{
return Math.Round(value, RoundingDigits, MidpointRounding.AwayFromZero);
}
private static dynamic TransformResult(object result)
{
if (result.ToString() == "NaN")
{
return Properties.Resources.calculator_not_a_number;
}
if (result is Function)
{
return Properties.Resources.calculator_expression_not_complete;
}
if (result is double[,])
{
// '[10,10]' is interpreted as array by mages engine
return Properties.Resources.calculator_double_array_returned;
}
return result;
}
}

View File

@@ -0,0 +1,328 @@
// 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.Text.RegularExpressions;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class CalculateHelper
{
private static readonly Regex RegValidExpressChar = new Regex(
@"^(" +
@"%|" +
@"ceil\s*\(|floor\s*\(|exp\s*\(|max\s*\(|min\s*\(|abs\s*\(|log(?:2|10)?\s*\(|ln\s*\(|sqrt\s*\(|pow\s*\(|" +
@"factorial\s*\(|sign\s*\(|round\s*\(|rand\s*\(\)|randi\s*\([^\)]|" +
@"sin\s*\(|cos\s*\(|tan\s*\(|arcsin\s*\(|arccos\s*\(|arctan\s*\(|" +
@"sinh\s*\(|cosh\s*\(|tanh\s*\(|arsinh\s*\(|arcosh\s*\(|artanh\s*\(|" +
@"rad\s*\(|deg\s*\(|grad\s*\(|" + /* trigonometry unit conversion macros */
@"pi|" +
@"==|~=|&&|\|\||" +
@"((-?(\d+(\.\d*)?)|-?(\.\d+))[Ee](-?\d+))|" + /* expression from CheckScientificNotation between parenthesis */
@"e|[0-9]|0[xX][0-9a-fA-F]+|0[bB][01]+|0[oO][0-7]+|[\+\-\*\/\^\., ""]|[\(\)\|\!\[\]]" +
@")+$",
RegexOptions.Compiled);
private const string DegToRad = "(pi / 180) * ";
private const string DegToGrad = "(10 / 9) * ";
private const string GradToRad = "(pi / 200) * ";
private const string GradToDeg = "(9 / 10) * ";
private const string RadToDeg = "(180 / pi) * ";
private const string RadToGrad = "(200 / pi) * ";
public static bool InputValid(string input)
{
if (string.IsNullOrWhiteSpace(input))
{
throw new ArgumentNullException(paramName: nameof(input));
}
if (!RegValidExpressChar.IsMatch(input))
{
return false;
}
if (!BracketHelper.IsBracketComplete(input))
{
return false;
}
// If the input ends with a binary operator then it is not a valid input to mages and the Interpret function would throw an exception. Because we expect here that the user has not finished typing we block those inputs.
var trimmedInput = input.TrimEnd();
if (trimmedInput.EndsWith('+') || trimmedInput.EndsWith('-') || trimmedInput.EndsWith('*') || trimmedInput.EndsWith('|') || trimmedInput.EndsWith('\\') || trimmedInput.EndsWith('^') || trimmedInput.EndsWith('=') || trimmedInput.EndsWith('&') || trimmedInput.EndsWith('/') || trimmedInput.EndsWith('%'))
{
return false;
}
return true;
}
public static string FixHumanMultiplicationExpressions(string input)
{
var output = CheckScientificNotation(input);
output = CheckNumberOrConstantThenParenthesisExpr(output);
output = CheckNumberOrConstantThenFunc(output);
output = CheckParenthesisExprThenFunc(output);
output = CheckParenthesisExprThenParenthesisExpr(output);
output = CheckNumberThenConstant(output);
output = CheckConstantThenConstant(output);
return output;
}
private static string CheckScientificNotation(string input)
{
/**
* NOTE: By the time the expression gets to us, it's already in English format.
*
* Regex explanation:
* (-?(\d+({0}\d*)?)|-?({0}\d+)): Used to capture one of two types:
* -?(\d+({0}\d*)?): Captures a decimal number starting with a number (e.g. "-1.23")
* -?({0}\d+): Captures a decimal number without leading number (e.g. ".23")
* e: Captures 'e' or 'E'
* (-?\d+): Captures an integer number (e.g. "-1" or "23")
*/
var p = @"(-?(\d+(\.\d*)?)|-?(\.\d+))e(-?\d+)";
return Regex.Replace(input, p, "($1 * 10^($5))", RegexOptions.IgnoreCase);
}
/*
* num (exp)
* const (exp)
*/
private static string CheckNumberOrConstantThenParenthesisExpr(string input)
{
var output = input;
do
{
input = output;
output = Regex.Replace(input, @"(\d+|pi|e)\s*(\()", m =>
{
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
{
return m.Value;
}
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
});
}
while (output != input);
return output;
}
/*
* num func
* const func
*/
private static string CheckNumberOrConstantThenFunc(string input)
{
var output = input;
do
{
input = output;
output = Regex.Replace(input, @"(\d+|pi|e)\s*([a-zA-Z]+[0-9]*\s*\()", m =>
{
if (input[m.Index] == 'e' && input[m.Index + 1] == 'x' && input[m.Index + 2] == 'p')
{
return m.Value;
}
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
{
return m.Value;
}
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
});
}
while (output != input);
return output;
}
/*
* (exp) func
* func func
*/
private static string CheckParenthesisExprThenFunc(string input)
{
var p = @"(\))\s*([a-zA-Z]+[0-9]*\s*\()";
var r = "$1 * $2";
return Regex.Replace(input, p, r);
}
/*
* (exp) (exp)
* func (exp)
*/
private static string CheckParenthesisExprThenParenthesisExpr(string input)
{
var p = @"(\))\s*(\()";
var r = "$1 * $2";
return Regex.Replace(input, p, r);
}
/*
* num const
*/
private static string CheckNumberThenConstant(string input)
{
var output = input;
do
{
input = output;
output = Regex.Replace(input, @"(\d+)\s*(pi|e)", m =>
{
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
{
return m.Value;
}
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
});
}
while (output != input);
return output;
}
/*
* const const
*/
private static string CheckConstantThenConstant(string input)
{
var output = input;
do
{
input = output;
output = Regex.Replace(input, @"(pi|e)\s*(pi|e)", m =>
{
if (m.Index > 0 && char.IsLetter(input[m.Index - 1]))
{
return m.Value;
}
return $"{m.Groups[1].Value} * {m.Groups[2].Value}";
});
}
while (output != input);
return output;
}
// Gets the index of the closing bracket of a function
private static int FindClosingBracketIndex(string input, int start)
{
var bracketCount = 0; // Set count to zero
for (var i = start; i < input.Length; i++)
{
if (input[i] == '(')
{
bracketCount++;
}
else if (input[i] == ')')
{
bracketCount--;
if (bracketCount == 0)
{
return i;
}
}
}
return -1; // Unmatched brackets
}
private static string ModifyTrigFunction(string input, string function, string modification)
{
// Get the RegEx pattern to match, depending on whether the function is inverse or normal
var pattern = function.StartsWith("arc", StringComparison.Ordinal) ? string.Empty : @"(?<!c)";
pattern += $@"{function}\s*\(";
var index = 0; // Index for match to ensure that the same match is not found twice
Regex regex = new Regex(pattern);
Match match;
while ((match = regex.Match(input, index)).Success)
{
index = match.Index + match.Groups[0].Length + modification.Length; // Get the next index to look from for further matches
var endIndex = FindClosingBracketIndex(input, match.Index + match.Groups[0].Length - 1); // Find the index of the closing bracket of the function
// If no valid bracket index was found, try the next match
if (endIndex == -1)
{
continue;
}
var argument = input.Substring(match.Index + match.Groups[0].Length, endIndex - (match.Index + match.Groups[0].Length)); // Extract the argument between the brackets
var replaced = function.StartsWith("arc", StringComparison.Ordinal) ? $"{modification}({match.Groups[0].Value}{argument}))" : $"{match.Groups[0].Value}{modification}({argument}))"; // The string to substitute in, handles differing formats of inverse functions
input = input.Remove(match.Index, endIndex - match.Index + 1); // Remove the match from the input
input = input.Insert(match.Index, replaced); // Substitute with the new string
}
return input;
}
public static string UpdateTrigFunctions(string input, CalculateEngine.TrigMode mode)
{
var modifiedInput = input;
if (mode == CalculateEngine.TrigMode.Degrees)
{
modifiedInput = ModifyTrigFunction(modifiedInput, "sin", DegToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "cos", DegToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "tan", DegToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "arcsin", RadToDeg);
modifiedInput = ModifyTrigFunction(modifiedInput, "arccos", RadToDeg);
modifiedInput = ModifyTrigFunction(modifiedInput, "arctan", RadToDeg);
}
else if (mode == CalculateEngine.TrigMode.Gradians)
{
modifiedInput = ModifyTrigFunction(modifiedInput, "sin", GradToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "cos", GradToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "tan", GradToRad);
modifiedInput = ModifyTrigFunction(modifiedInput, "arcsin", RadToGrad);
modifiedInput = ModifyTrigFunction(modifiedInput, "arccos", RadToGrad);
modifiedInput = ModifyTrigFunction(modifiedInput, "arctan", RadToGrad);
}
return modifiedInput;
}
private static string ModifyMathFunction(string input, string function, string modification)
{
// Create the pattern to match the function, opening bracket, and any spaces in between
var pattern = $@"{function}\s*\(";
return Regex.Replace(input, pattern, modification + "(");
}
public static string ExpandTrigConversions(string input, CalculateEngine.TrigMode mode)
{
var modifiedInput = input;
// Expand "rad", "deg" and "grad" to their respective conversions for the current trig unit
if (mode == CalculateEngine.TrigMode.Radians)
{
modifiedInput = ModifyMathFunction(modifiedInput, "deg", DegToRad);
modifiedInput = ModifyMathFunction(modifiedInput, "grad", GradToRad);
modifiedInput = ModifyMathFunction(modifiedInput, "rad", string.Empty);
}
else if (mode == CalculateEngine.TrigMode.Degrees)
{
modifiedInput = ModifyMathFunction(modifiedInput, "deg", string.Empty);
modifiedInput = ModifyMathFunction(modifiedInput, "grad", GradToDeg);
modifiedInput = ModifyMathFunction(modifiedInput, "rad", RadToDeg);
}
else if (mode == CalculateEngine.TrigMode.Gradians)
{
modifiedInput = ModifyMathFunction(modifiedInput, "deg", DegToGrad);
modifiedInput = ModifyMathFunction(modifiedInput, "grad", string.Empty);
modifiedInput = ModifyMathFunction(modifiedInput, "rad", RadToGrad);
}
return modifiedInput;
}
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public struct CalculateResult : IEquatable<CalculateResult>
{
public decimal? Result { get; set; }
public decimal? RoundedResult { get; set; }
public bool Equals(CalculateResult other)
{
return Result == other.Result && RoundedResult == other.RoundedResult;
}
public override bool Equals(object obj)
{
return obj is CalculateResult other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(Result, RoundedResult);
}
public static bool operator ==(CalculateResult left, CalculateResult right)
{
return left.Equals(right);
}
public static bool operator !=(CalculateResult left, CalculateResult right)
{
return !(left == right);
}
}

View File

@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class CalculatorIcons
{
public static IconInfo ResultIcon => new("\uE94E");
public static IconInfo SaveIcon => new("\uE74E");
public static IconInfo ErrorIcon => new("\uE783");
public static IconInfo ProviderIcon => IconHelpers.FromRelativePath("Assets\\Calculator.svg");
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
internal static class ErrorHandler
{
/// <summary>
/// Method to handles errors while calculating
/// </summary>
/// <param name="isFallbackSearch">Bool to indicate if it is a fallback query.</param>
/// <param name="queryInput">User input as string including the action keyword.</param>
/// <param name="errorMessage">Error message if applicable.</param>
/// <param name="exception">Exception if applicable.</param>
/// <returns>List of results to show. Either an error message or an empty list.</returns>
/// <exception cref="ArgumentException">Thrown if <paramref name="errorMessage"/> and <paramref name="exception"/> are both filled with their default values.</exception>
internal static ListItem OnError(bool isFallbackSearch, string queryInput, string errorMessage, Exception exception = default)
{
string userMessage;
if (errorMessage != default)
{
Logger.LogError($"Failed to calculate <{queryInput}>: {errorMessage}");
userMessage = errorMessage;
}
else if (exception != default)
{
Logger.LogError($"Exception when query for <{queryInput}>", exception);
userMessage = exception.Message;
}
else
{
throw new ArgumentException("The arguments error and exception have default values. One of them has to be filled with valid error data (error message/exception)!");
}
return isFallbackSearch ? null : CreateErrorResult(userMessage);
}
private static ListItem CreateErrorResult(string errorMessage)
{
return new ListItem(new NoOpCommand())
{
Title = Properties.Resources.calculator_calculation_failed_title,
Subtitle = errorMessage,
Icon = CalculatorIcons.ErrorIcon,
};
}
}

View File

@@ -0,0 +1,144 @@
// 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.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
/// <summary>
/// Tries to convert all numbers in a text from one culture format to another.
/// </summary>
public class NumberTranslator
{
private readonly CultureInfo sourceCulture;
private readonly CultureInfo targetCulture;
private readonly Regex splitRegexForSource;
private readonly Regex splitRegexForTarget;
private NumberTranslator(CultureInfo sourceCulture, CultureInfo targetCulture)
{
this.sourceCulture = sourceCulture;
this.targetCulture = targetCulture;
splitRegexForSource = GetSplitRegex(this.sourceCulture);
splitRegexForTarget = GetSplitRegex(this.targetCulture);
}
/// <summary>
/// Create a new <see cref="NumberTranslator"/>.
/// </summary>
/// <param name="sourceCulture">source culture</param>
/// <param name="targetCulture">target culture</param>
/// <returns>Number translator for target culture</returns>
public static NumberTranslator Create(CultureInfo sourceCulture, CultureInfo targetCulture)
{
ArgumentNullException.ThrowIfNull(sourceCulture);
ArgumentNullException.ThrowIfNull(targetCulture);
return new NumberTranslator(sourceCulture, targetCulture);
}
/// <summary>
/// Translate from source to target culture.
/// </summary>
/// <param name="input">input string to translate</param>
/// <returns>translated string</returns>
public string Translate(string input)
{
return Translate(input, sourceCulture, targetCulture, splitRegexForSource);
}
/// <summary>
/// Translate from target to source culture.
/// </summary>
/// <param name="input">input string to translate back to source culture</param>
/// <returns>source culture string</returns>
public string TranslateBack(string input)
{
return Translate(input, targetCulture, sourceCulture, splitRegexForTarget);
}
private static string Translate(string input, CultureInfo cultureFrom, CultureInfo cultureTo, Regex splitRegex)
{
var outputBuilder = new StringBuilder();
var hexRegex = new Regex(@"(?:(0x[\da-fA-F]+))");
var hexTokens = hexRegex.Split(input);
foreach (var hexToken in hexTokens)
{
if (hexToken.StartsWith("0x", StringComparison.InvariantCultureIgnoreCase))
{
// Mages engine has issues processing large hex number (larger than 7 hex digits + 0x prefix = 9 characters). So we convert it to decimal and pass it to the engine.
if (hexToken.Length > 9)
{
try
{
var num = Convert.ToInt64(hexToken, 16);
var numStr = num.ToString(cultureFrom);
outputBuilder.Append(numStr);
}
catch (Exception)
{
outputBuilder.Append(hexToken);
}
}
else
{
outputBuilder.Append(hexToken);
}
continue;
}
var tokens = splitRegex.Split(hexToken);
foreach (var token in tokens)
{
var leadingZeroCount = 0;
// Count leading zero characters.
foreach (var c in token)
{
if (c != '0')
{
break;
}
leadingZeroCount++;
}
// number is all zero characters. no need to add zero characters at the end.
if (token.Length == leadingZeroCount)
{
leadingZeroCount = 0;
}
decimal number;
outputBuilder.Append(
decimal.TryParse(token, NumberStyles.Number, cultureFrom, out number)
? (new string('0', leadingZeroCount) + number.ToString(cultureTo))
: token.Replace(cultureFrom.TextInfo.ListSeparator, cultureTo.TextInfo.ListSeparator));
}
}
return outputBuilder.ToString();
}
private static Regex GetSplitRegex(CultureInfo culture)
{
var splitPattern = $"((?:\\d|{Regex.Escape(culture.NumberFormat.NumberDecimalSeparator)}";
if (!string.IsNullOrEmpty(culture.NumberFormat.NumberGroupSeparator))
{
splitPattern += $"|{Regex.Escape(culture.NumberFormat.NumberGroupSeparator)}";
}
splitPattern += ")+)";
return new Regex(splitPattern);
}
}

View File

@@ -0,0 +1,77 @@
// 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.Globalization;
using System.Text;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static partial class QueryHelper
{
public static ListItem Query(string query, SettingsManager settings, bool isFallbackSearch, TypedEventHandler<object, object> handleSave = null)
{
ArgumentNullException.ThrowIfNull(query);
if (!isFallbackSearch)
{
ArgumentNullException.ThrowIfNull(handleSave);
}
CultureInfo inputCulture = settings.InputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
CultureInfo outputCulture = settings.OutputUseEnglishFormat ? new CultureInfo("en-us") : CultureInfo.CurrentCulture;
// Happens if the user has only typed the action key so far
if (string.IsNullOrEmpty(query))
{
return null;
}
NumberTranslator translator = NumberTranslator.Create(inputCulture, new CultureInfo("en-US"));
var input = translator.Translate(query.Normalize(NormalizationForm.FormKC));
if (!CalculateHelper.InputValid(input))
{
return null;
}
try
{
// Using CurrentUICulture since this is user facing
var result = CalculateEngine.Interpret(settings, input, outputCulture, out var errorMessage);
// This could happen for some incorrect queries, like pi(2)
if (result.Equals(default(CalculateResult)))
{
// If errorMessage is not default then do error handling
return errorMessage == default ? null : ErrorHandler.OnError(isFallbackSearch, query, errorMessage);
}
if (isFallbackSearch)
{
// Fallback search
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query);
}
return ResultHelper.CreateResult(result.RoundedResult, inputCulture, outputCulture, query, handleSave);
}
catch (Mages.Core.ParseException)
{
// Invalid input
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_expression_not_complete);
}
catch (OverflowException)
{
// Result to big to convert to decimal
return ErrorHandler.OnError(isFallbackSearch, query, Properties.Resources.calculator_not_covert_to_decimal);
}
catch (Exception e)
{
// Any other crash occurred
// We want to keep the process alive if any the mages library throws any exceptions.
return ErrorHandler.OnError(isFallbackSearch, query, default, e);
}
}
}

View File

@@ -0,0 +1,103 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Globalization;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public static class ResultHelper
{
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query, TypedEventHandler<object, object> handleSave)
{
// Return null when the expression is not a valid calculator query.
if (roundedResult == null)
{
return null;
}
var result = roundedResult?.ToString(outputCulture);
// Create a SaveCommand and subscribe to the SaveRequested event
// This can append the result to the history list.
var saveCommand = new SaveCommand(result);
saveCommand.SaveRequested += handleSave;
var copyCommandItem = CreateResult(roundedResult, inputCulture, outputCulture, query);
return new ListItem(saveCommand)
{
// Using CurrentCulture since this is user facing
Icon = CalculatorIcons.ResultIcon,
Title = result,
Subtitle = query,
TextToSuggest = result,
MoreCommands = [
new CommandContextItem(copyCommandItem.Command)
{
Icon = copyCommandItem.Icon,
Title = copyCommandItem.Title,
Subtitle = copyCommandItem.Subtitle,
},
..copyCommandItem.MoreCommands,
],
};
}
public static ListItem CreateResult(decimal? roundedResult, CultureInfo inputCulture, CultureInfo outputCulture, string query)
{
// Return null when the expression is not a valid calculator query.
if (roundedResult == null)
{
return null;
}
var decimalResult = roundedResult?.ToString(outputCulture);
List<CommandContextItem> context = [];
if (decimal.IsInteger((decimal)roundedResult))
{
var i = decimal.ToInt64((decimal)roundedResult);
try
{
var hexResult = "0x" + i.ToString("X", outputCulture);
context.Add(new CommandContextItem(new CopyTextCommand(hexResult) { Name = Properties.Resources.calculator_copy_hex })
{
Title = hexResult,
});
}
catch (Exception ex)
{
Logger.LogError("Error parsing hex format", ex);
}
try
{
var binaryResult = "0b" + i.ToString("B", outputCulture);
context.Add(new CommandContextItem(new CopyTextCommand(binaryResult) { Name = Properties.Resources.calculator_copy_binary })
{
Title = binaryResult,
});
}
catch (Exception ex)
{
Logger.LogError("Error parsing binary format", ex);
}
}
return new ListItem(new CopyTextCommand(decimalResult))
{
// Using CurrentCulture since this is user facing
Title = decimalResult,
Subtitle = query,
TextToSuggest = decimalResult,
MoreCommands = context.ToArray(),
};
}
}

View File

@@ -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.Calc.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public sealed partial class SaveCommand : InvokableCommand
{
private readonly string _result;
public event TypedEventHandler<object, object> SaveRequested;
public SaveCommand(string result)
{
Name = Resources.calculator_save_command_name;
Icon = CalculatorIcons.SaveIcon;
_result = result;
}
public override ICommandResult Invoke()
{
SaveRequested?.Invoke(this, this);
ClipboardHelper.SetText(_result);
return CommandResult.KeepOpen();
}
}

View File

@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Helper;
public class SettingsManager : JsonSettingsManager
{
private static readonly string _namespace = "calculator";
private static string Namespaced(string propertyName) => $"{_namespace}.{propertyName}";
private static readonly List<ChoiceSetSetting.Choice> _trigUnitChoices = new()
{
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_radians, "0"),
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_degrees, "1"),
new ChoiceSetSetting.Choice(Properties.Resources.calculator_settings_trig_unit_gradians, "2"),
};
private readonly ChoiceSetSetting _trigUnit = new(
Namespaced(nameof(TrigUnit)),
Properties.Resources.calculator_settings_trig_unit_mode,
Properties.Resources.calculator_settings_trig_unit_mode_description,
_trigUnitChoices);
private readonly ToggleSetting _inputUseEnNumberFormat = new(
Namespaced(nameof(InputUseEnglishFormat)),
Properties.Resources.calculator_settings_in_en_format,
Properties.Resources.calculator_settings_in_en_format_description,
false);
private readonly ToggleSetting _outputUseEnNumberFormat = new(
Namespaced(nameof(OutputUseEnglishFormat)),
Properties.Resources.calculator_settings_out_en_format,
Properties.Resources.calculator_settings_out_en_format_description,
false);
public CalculateEngine.TrigMode TrigUnit
{
get
{
if (_trigUnit.Value == null || string.IsNullOrEmpty(_trigUnit.Value))
{
return CalculateEngine.TrigMode.Radians;
}
var success = int.TryParse(_trigUnit.Value, out var result);
if (!success)
{
return CalculateEngine.TrigMode.Radians;
}
switch (result)
{
case 0:
return CalculateEngine.TrigMode.Radians;
case 1:
return CalculateEngine.TrigMode.Degrees;
case 2:
return CalculateEngine.TrigMode.Gradians;
default:
return CalculateEngine.TrigMode.Radians;
}
}
}
public bool InputUseEnglishFormat => _inputUseEnNumberFormat.Value;
public bool OutputUseEnglishFormat => _outputUseEnNumberFormat.Value;
internal static string SettingsJsonPath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
// now, the state is just next to the exe
return Path.Combine(directory, "settings.json");
}
public SettingsManager()
{
FilePath = SettingsJsonPath();
Settings.Add(_trigUnit);
Settings.Add(_inputUseEnNumberFormat);
Settings.Add(_outputUseEnNumberFormat);
// Load settings from file upon initialization
LoadSettings();
Settings.SettingsChanged += (s, a) => this.SaveSettings();
}
}

View File

@@ -9,9 +9,14 @@
<ProjectPriFileName>Microsoft.CmdPal.Ext.Calc.pri</ProjectPriFileName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Mages" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DependentUpon>Resources.resx</DependentUpon>

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Threading;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Properties;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Pages;
// The calculator page is a dynamic list page
// * The first command is where we display the results. Title=result, Subtitle=query
// - The default command is `SaveCommand`.
// - When you save, insert into list at spot 1
// - change SearchText to the result
// - MoreCommands: a single `CopyCommand` to copy the result to the clipboard
// * The rest of the items are previously saved results
// - Command is a CopyCommand
// - Each item also sets the TextToSuggest to the result
public sealed partial class CalculatorListPage : DynamicListPage
{
private readonly Lock _resultsLock = new();
private readonly SettingsManager _settingsManager;
private readonly List<ListItem> _items = [];
private readonly List<ListItem> history = [];
private readonly ListItem _emptyItem;
// This is the text that saved when the user click the result.
// We need to avoid the double calculation. This may cause some wierd behaviors.
private string skipQuerySearchText = string.Empty;
public CalculatorListPage(SettingsManager settings)
{
_settingsManager = settings;
Icon = CalculatorIcons.ProviderIcon;
Name = Resources.calculator_title;
PlaceholderText = Resources.calculator_placeholder_text;
Id = "com.microsoft.cmdpal.calculator";
_emptyItem = new ListItem(new NoOpCommand())
{
Title = Resources.calculator_placeholder_text,
Icon = CalculatorIcons.ResultIcon,
};
EmptyContent = new CommandItem(new NoOpCommand())
{
Icon = CalculatorIcons.ProviderIcon,
Title = Resources.calculator_placeholder_text,
};
UpdateSearchText(string.Empty, string.Empty);
}
public override void UpdateSearchText(string oldSearch, string newSearch)
{
if (oldSearch == newSearch)
{
return;
}
if (!string.IsNullOrEmpty(skipQuerySearchText) && newSearch == skipQuerySearchText)
{
// only skip once.
skipQuerySearchText = string.Empty;
return;
}
skipQuerySearchText = string.Empty;
_emptyItem.Subtitle = newSearch;
var result = QueryHelper.Query(newSearch, _settingsManager, false, HandleSave);
UpdateResult(result);
}
private void UpdateResult(ListItem result)
{
lock (_resultsLock)
{
this._items.Clear();
if (result != null)
{
this._items.Add(result);
}
else
{
_items.Add(_emptyItem);
}
this._items.AddRange(history);
}
RaiseItemsChanged(this._items.Count);
}
private void HandleSave(object sender, object args)
{
var lastResult = _items[0].Title;
if (!string.IsNullOrEmpty(lastResult))
{
var li = new ListItem(new CopyTextCommand(lastResult))
{
Title = _items[0].Title,
Subtitle = _items[0].Subtitle,
TextToSuggest = lastResult,
};
history.Insert(0, li);
_items.Insert(1, li);
// Why we need to clean the query record? Removed, but if necessary, please move it back.
// _items[0].Subtitle = string.Empty;
// this change will call the UpdateSearchText again.
// We need to avoid it.
skipQuerySearchText = lastResult;
SearchText = lastResult;
this.RaiseItemsChanged(this._items.Count);
}
}
public override IListItem[] GetItems() => _items.ToArray();
}

View File

@@ -0,0 +1,52 @@
// 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.Calc.Helper;
using Microsoft.CmdPal.Ext.Calc.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Calc.Pages;
public sealed partial class FallbackCalculatorItem : FallbackCommandItem
{
private readonly CopyTextCommand _copyCommand = new(string.Empty);
private readonly SettingsManager _settings;
public FallbackCalculatorItem(SettingsManager settings)
: base(new NoOpCommand(), Resources.calculator_title)
{
Command = _copyCommand;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = Resources.calculator_placeholder_text;
Icon = CalculatorIcons.ProviderIcon;
_settings = settings;
}
public override void UpdateQuery(string query)
{
var result = QueryHelper.Query(query, _settings, true, null);
if (result == null)
{
_copyCommand.Text = string.Empty;
_copyCommand.Name = string.Empty;
Title = string.Empty;
Subtitle = string.Empty;
MoreCommands = [];
return;
}
_copyCommand.Text = result.Title;
_copyCommand.Name = string.IsNullOrWhiteSpace(query) ? string.Empty : Resources.calculator_copy_command_name;
Title = result.Title;
// we have to make the subtitle the equation,
// so that we will still string match the original query
// Otherwise, something like 1+2 will have a title of "3" and not match
Subtitle = query;
MoreCommands = result.MoreCommands;
}
}

View File

@@ -60,6 +60,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Failed to calculate the input.
/// </summary>
public static string calculator_calculation_failed_title {
get {
return ResourceManager.GetString("calculator_calculation_failed_title", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy binary.
/// </summary>
public static string calculator_copy_binary {
get {
return ResourceManager.GetString("calculator_copy_binary", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Copy.
/// </summary>
@@ -69,6 +87,15 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Copy hexadecimal.
/// </summary>
public static string calculator_copy_hex {
get {
return ResourceManager.GetString("calculator_copy_hex", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculator.
/// </summary>
@@ -78,6 +105,24 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Expression contains division by zero.
/// </summary>
public static string calculator_division_by_zero {
get {
return ResourceManager.GetString("calculator_division_by_zero", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unsupported use of square brackets.
/// </summary>
public static string calculator_double_array_returned {
get {
return ResourceManager.GetString("calculator_double_array_returned", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Error: {0}.
/// </summary>
@@ -87,6 +132,33 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Expression wrong or incomplete.
/// </summary>
public static string calculator_expression_not_complete {
get {
return ResourceManager.GetString("calculator_expression_not_complete", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculation result is not a valid number (NaN).
/// </summary>
public static string calculator_not_a_number {
get {
return ResourceManager.GetString("calculator_not_a_number", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Result value was either too large or too small for a decimal number.
/// </summary>
public static string calculator_not_covert_to_decimal {
get {
return ResourceManager.GetString("calculator_not_covert_to_decimal", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Type an equation....
/// </summary>
@@ -105,6 +177,105 @@ namespace Microsoft.CmdPal.Ext.Calc.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Use English (United States) number format for input.
/// </summary>
public static string calculator_settings_in_en_format {
get {
return ResourceManager.GetString("calculator_settings_in_en_format", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignores your system setting and expects numbers in the format &apos;{0}&apos;..
/// </summary>
public static string calculator_settings_in_en_format_description {
get {
return ResourceManager.GetString("calculator_settings_in_en_format_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use English (United States) number format for output.
/// </summary>
public static string calculator_settings_out_en_format {
get {
return ResourceManager.GetString("calculator_settings_out_en_format", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Ignores your system setting and returns numbers in the format &apos;{0}&apos;..
/// </summary>
public static string calculator_settings_out_en_format_description {
get {
return ResourceManager.GetString("calculator_settings_out_en_format_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Replace input if query ends with &apos;=&apos;.
/// </summary>
public static string calculator_settings_replace_input {
get {
return ResourceManager.GetString("calculator_settings_replace_input", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to When using direct activation, appending &apos;=&apos; to the expression will replace the input with the calculated result (e.g. &apos;=5*3-2=&apos; will change the query to &apos;=13&apos;)..
/// </summary>
public static string calculator_settings_replace_input_description {
get {
return ResourceManager.GetString("calculator_settings_replace_input_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Degrees.
/// </summary>
public static string calculator_settings_trig_unit_degrees {
get {
return ResourceManager.GetString("calculator_settings_trig_unit_degrees", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Gradians.
/// </summary>
public static string calculator_settings_trig_unit_gradians {
get {
return ResourceManager.GetString("calculator_settings_trig_unit_gradians", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Trigonometry Unit.
/// </summary>
public static string calculator_settings_trig_unit_mode {
get {
return ResourceManager.GetString("calculator_settings_trig_unit_mode", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Specifies the angle unit to use for trigonometry operations.
/// </summary>
public static string calculator_settings_trig_unit_mode_description {
get {
return ResourceManager.GetString("calculator_settings_trig_unit_mode_description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Radians.
/// </summary>
public static string calculator_settings_trig_unit_radians {
get {
return ResourceManager.GetString("calculator_settings_trig_unit_radians", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Calculator.
/// </summary>

View File

@@ -140,4 +140,63 @@
<data name="calculator_copy_command_name" xml:space="preserve">
<value>Copy</value>
</data>
<data name="calculator_calculation_failed_title" xml:space="preserve">
<value>Failed to calculate the input</value>
</data>
<data name="calculator_division_by_zero" xml:space="preserve">
<value>Expression contains division by zero</value>
</data>
<data name="calculator_expression_not_complete" xml:space="preserve">
<value>Expression wrong or incomplete</value>
</data>
<data name="calculator_not_a_number" xml:space="preserve">
<value>Calculation result is not a valid number (NaN)</value>
</data>
<data name="calculator_double_array_returned" xml:space="preserve">
<value>Unsupported use of square brackets</value>
</data>
<data name="calculator_settings_trig_unit_gradians" xml:space="preserve">
<value>Gradians</value>
</data>
<data name="calculator_settings_trig_unit_degrees" xml:space="preserve">
<value>Degrees</value>
</data>
<data name="calculator_settings_trig_unit_radians" xml:space="preserve">
<value>Radians</value>
</data>
<data name="calculator_settings_trig_unit_mode" xml:space="preserve">
<value>Trigonometry Unit</value>
</data>
<data name="calculator_settings_trig_unit_mode_description" xml:space="preserve">
<value>Specifies the angle unit to use for trigonometry operations</value>
</data>
<data name="calculator_settings_out_en_format" xml:space="preserve">
<value>Use English (United States) number format for output</value>
</data>
<data name="calculator_settings_out_en_format_description" xml:space="preserve">
<value>Ignores your system setting and returns numbers in the format '{0}'.</value>
<comment>{0} is a placeholder and will be replaced in code.</comment>
</data>
<data name="calculator_settings_in_en_format" xml:space="preserve">
<value>Use English (United States) number format for input</value>
</data>
<data name="calculator_settings_in_en_format_description" xml:space="preserve">
<value>Ignores your system setting and expects numbers in the format '{0}'.</value>
<comment>{0} is a placeholder and will be replaced in code.</comment>
</data>
<data name="calculator_settings_replace_input" xml:space="preserve">
<value>Replace input if query ends with '='</value>
</data>
<data name="calculator_settings_replace_input_description" xml:space="preserve">
<value>When using direct activation, appending '=' to the expression will replace the input with the calculated result (e.g. '=5*3-2=' will change the query to '=13').</value>
</data>
<data name="calculator_not_covert_to_decimal" xml:space="preserve">
<value>Result value was either too large or too small for a decimal number</value>
</data>
<data name="calculator_copy_hex" xml:space="preserve">
<value>Copy hexadecimal</value>
</data>
<data name="calculator_copy_binary" xml:space="preserve">
<value>Copy binary</value>
</data>
</root>

View File

@@ -22,10 +22,12 @@ public partial class InstallPackageCommand : InvokableCommand
private IAsyncOperationWithProgress<UninstallResult, UninstallProgress>? _unInstallAction;
private Task? _installTask;
public bool IsInstalled { get; private set; }
public PackageInstallCommandState InstallCommandState { get; private set; }
public static IconInfo CompletedIcon { get; } = new("\uE930"); // Completed
public static IconInfo UpdateIcon { get; } = new("\uE74A"); // Up
public static IconInfo DownloadIcon { get; } = new("\uE896"); // Download
public static IconInfo DeleteIcon { get; } = new("\uE74D"); // Delete
@@ -44,23 +46,41 @@ public partial class InstallPackageCommand : InvokableCommand
internal bool SkipDependencies { get; set; }
public InstallPackageCommand(CatalogPackage package, bool isInstalled)
public InstallPackageCommand(CatalogPackage package, PackageInstallCommandState isInstalled)
{
_package = package;
IsInstalled = isInstalled;
InstallCommandState = isInstalled;
UpdateAppearance();
}
internal void FakeChangeStatus()
{
IsInstalled = !IsInstalled;
InstallCommandState = InstallCommandState switch
{
PackageInstallCommandState.Install => PackageInstallCommandState.Uninstall,
PackageInstallCommandState.Update => PackageInstallCommandState.Uninstall,
PackageInstallCommandState.Uninstall => PackageInstallCommandState.Install,
_ => throw new NotImplementedException(),
};
UpdateAppearance();
}
private void UpdateAppearance()
{
Icon = IsInstalled ? CompletedIcon : DownloadIcon;
Name = IsInstalled ? Properties.Resources.winget_uninstall_name : Properties.Resources.winget_install_name;
Icon = InstallCommandState switch
{
PackageInstallCommandState.Install => DownloadIcon,
PackageInstallCommandState.Update => UpdateIcon,
PackageInstallCommandState.Uninstall => CompletedIcon,
_ => throw new NotImplementedException(),
};
Name = InstallCommandState switch
{
PackageInstallCommandState.Install => Properties.Resources.winget_install_name,
PackageInstallCommandState.Update => Properties.Resources.winget_update_name,
PackageInstallCommandState.Uninstall => Properties.Resources.winget_uninstall_name,
_ => throw new NotImplementedException(),
};
}
public override ICommandResult Invoke()
@@ -72,7 +92,7 @@ public partial class InstallPackageCommand : InvokableCommand
return CommandResult.KeepOpen();
}
if (IsInstalled)
if (InstallCommandState == PackageInstallCommandState.Uninstall)
{
// Uninstall
_installBanner.State = MessageState.Info;
@@ -88,7 +108,8 @@ public partial class InstallPackageCommand : InvokableCommand
_installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction));
}
else
else if (InstallCommandState is PackageInstallCommandState.Install or
PackageInstallCommandState.Update)
{
// Install
_installBanner.State = MessageState.Info;
@@ -117,7 +138,8 @@ public partial class InstallPackageCommand : InvokableCommand
try
{
await action.AsTask();
_installBanner.Message = IsInstalled ?
_installBanner.Message = InstallCommandState == PackageInstallCommandState.Uninstall ?
string.Format(CultureInfo.CurrentCulture, UninstallPackageFinished, _package.Name) :
string.Format(CultureInfo.CurrentCulture, InstallPackageFinished, _package.Name);
@@ -125,9 +147,10 @@ public partial class InstallPackageCommand : InvokableCommand
_installBanner.State = MessageState.Success;
_installTask = null;
_ = Task.Run(() =>
_ = Task.Run(async () =>
{
Thread.Sleep(2500);
await Task.Delay(2500).ConfigureAwait(false);
if (_installTask == null)
{
WinGetExtensionHost.Instance.HideStatus(_installBanner);
@@ -228,3 +251,10 @@ public partial class InstallPackageCommand : InvokableCommand
}
}
}
public enum PackageInstallCommandState
{
Uninstall = 0,
Update = 1,
Install = 2,
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
@@ -31,7 +32,7 @@ public partial class InstallPackageListItem : ListItem
{
_package = package;
var version = _package.DefaultInstallVersion;
var version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
var versionTagText = "Unknown";
if (version != null)
{
@@ -49,7 +50,16 @@ public partial class InstallPackageListItem : ListItem
private Details? BuildDetails(PackageVersionInfo? version)
{
var metadata = version?.GetCatalogPackageMetadata();
CatalogPackageMetadata? metadata = null;
try
{
metadata = version?.GetCatalogPackageMetadata();
}
catch (COMException ex)
{
Logger.LogWarning($"{ex.ErrorCode}");
}
if (metadata != null)
{
if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any())
@@ -149,12 +159,17 @@ public partial class InstallPackageListItem : ListItem
var status = await _package.CheckInstalledStatusAsync();
var isInstalled = _package.InstalledVersion != null;
var installedState = isInstalled ?
(_package.IsUpdateAvailable ?
PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
PackageInstallCommandState.Install;
// might be an uninstall command
InstallPackageCommand installCommand = new(_package, isInstalled);
InstallPackageCommand installCommand = new(_package, installedState);
if (isInstalled)
{
this.Icon = InstallPackageCommand.CompletedIcon;
this.Icon = installCommand.Icon;
this.Command = new NoOpCommand();
List<IContextItem> contextMenu = [];
CommandContextItem uninstallContextItem = new(installCommand)
@@ -180,7 +195,7 @@ public partial class InstallPackageListItem : ListItem
}
// didn't find the app
_installCommand = new InstallPackageCommand(_package, isInstalled);
_installCommand = new InstallPackageCommand(_package, installedState);
this.Command = _installCommand;
Icon = _installCommand.Icon;

View File

@@ -330,6 +330,15 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Update.
/// </summary>
public static string winget_update_name {
get {
return ResourceManager.GetString("winget_update_name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to View online.
/// </summary>

View File

@@ -154,6 +154,10 @@
<value>Install</value>
<comment></comment>
</data>
<data name="winget_update_name" xml:space="preserve">
<value>Update</value>
<comment></comment>
</data>
<data name="winget_uninstalling_package" xml:space="preserve">
<value>Uninstalling {0}...</value>
<comment>{0} will be replaced by the name of an app package</comment>

View File

@@ -0,0 +1,3 @@
GetForegroundWindow
GetWindowTextLength
GetWindowText

View File

@@ -4,6 +4,8 @@
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
using Windows.Win32;
namespace SamplePagesExtension;
@@ -65,6 +67,68 @@ internal sealed partial class SampleListPage : ListPage
Subtitle = "and I'll take you to a page with markdown content",
Tags = [new Tag("Sample Tag")],
},
new ListItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Primary command invoked",
State = MessageState.Info,
});
t.Show();
})
{
Result = CommandResult.KeepOpen(),
Icon = new IconInfo("\uE712"),
})
{
Title = "You can add context menu items too. Press Ctrl+k",
Subtitle = "Try pressing Ctrl+1 with me selected",
Icon = new IconInfo("\uE712"),
MoreCommands = [
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Secondary command invoked",
State = MessageState.Warning,
});
t.Show();
})
{
Name = "Secondary command",
Icon = new IconInfo("\uF147"), // Dial 2
Result = CommandResult.KeepOpen(),
})
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(
new AnonymousCommand(() =>
{
var t = new ToastStatusMessage(new StatusMessage()
{
Message = "Third command invoked",
State = MessageState.Error,
});
t.Show();
})
{
Name = "Do it",
Icon = new IconInfo("\uF148"), // dial 3
Result = CommandResult.KeepOpen(),
})
{
Title = "A third command too",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
}
],
},
new ListItem(new SendMessageCommand())
{
Title = "I send lots of messages",
@@ -91,7 +155,35 @@ internal sealed partial class SampleListPage : ListPage
})
{
Title = "Confirm twice before doing something",
}
},
new ListItem(
new AnonymousCommand(() =>
{
var fg = PInvoke.GetForegroundWindow();
var bufferSize = PInvoke.GetWindowTextLength(fg) + 1;
unsafe
{
fixed (char* windowNameChars = new char[bufferSize])
{
if (PInvoke.GetWindowText(fg, windowNameChars, bufferSize) == 0)
{
var emptyToast = new ToastStatusMessage(new StatusMessage() { Message = "FG Window didn't have a title", State = MessageState.Warning });
emptyToast.Show();
}
var windowName = new string(windowNameChars);
var nameToast = new ToastStatusMessage(new StatusMessage() { Message = $"FG Window is {windowName}", State = MessageState.Success });
nameToast.Show();
}
}
})
{
Result = CommandResult.KeepOpen(),
})
{
Title = "Get the name of the Foreground window",
},
];
}
}

View File

@@ -33,6 +33,12 @@
<ProjectReference Include="..\..\extensionsdk\Microsoft.CommandPalette.Extensions.Toolkit\Microsoft.CommandPalette.Extensions.Toolkit.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget

View File

@@ -2,14 +2,19 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Windows.Foundation;
using Windows.System;
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class KeyChordHelpers
{
public static KeyChord FromModifiers(bool ctrl, bool alt, bool shift, bool win, int vkey, int scanCode)
public static KeyChord FromModifiers(
bool ctrl = false,
bool alt = false,
bool shift = false,
bool win = false,
int vkey = 0,
int scanCode = 0)
{
var modifiers = (ctrl ? VirtualKeyModifiers.Control : VirtualKeyModifiers.None)
| (alt ? VirtualKeyModifiers.Menu : VirtualKeyModifiers.None)
@@ -18,4 +23,15 @@ public partial class KeyChordHelpers
;
return new(modifiers, vkey, scanCode);
}
public static KeyChord FromModifiers(
bool ctrl = false,
bool alt = false,
bool shift = false,
bool win = false,
VirtualKey vkey = VirtualKey.None,
int scanCode = 0)
{
return FromModifiers(ctrl, alt, shift, win, (int)vkey, scanCode);
}
}