Users can now pin/unpin apps from the top of AllApps extension (#40544)

Users can now use Ctrl+P to toggle pinning and unpinning applications
from the top of the All Apps extension.

This pinned status persists through restarts & reboots.


https://github.com/user-attachments/assets/86895a38-7312-438a-9409-b50a85979d12

Reference: #40543

---------

Co-authored-by: Mike Griese <migrie@microsoft.com>
This commit is contained in:
Michael Jolley
2025-07-15 08:59:32 -05:00
committed by GitHub
parent 5800b81638
commit 19390e3198
20 changed files with 548 additions and 88 deletions

View File

@@ -2,8 +2,12 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -29,9 +33,12 @@ public partial class AllAppsCommandProvider : CommandProvider
Subtitle = Resources.search_installed_apps,
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
};
// Subscribe to pin state changes to refresh the command provider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
}
public override ICommandItem[] TopLevelCommands() => [_listItem];
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName)
{
@@ -62,4 +69,9 @@ public partial class AllAppsCommandProvider : CommandProvider
return null;
}
private void OnPinStateChanged(object? sender, System.EventArgs e)
{
RaiseItemsChanged(0);
}
}

View File

@@ -2,14 +2,20 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -18,16 +24,22 @@ namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage
{
private readonly Lock _listLock = new();
private AppListItem[] allAppsSection = [];
private AppItem[] allApps = [];
private AppListItem[] unpinnedApps = [];
private AppListItem[] pinnedApps = [];
public AllAppsPage()
{
this.Name = Resources.all_apps;
this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
this.Icon = Icons.AllAppsIcon;
this.ShowDetails = true;
this.IsLoading = true;
this.PlaceholderText = Resources.search_installed_apps_placeholder;
// Subscribe to pin state changes to refresh the command provider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
Task.Run(() =>
{
lock (_listLock)
@@ -37,20 +49,24 @@ public sealed partial class AllAppsPage : ListPage
});
}
public override IListItem[] GetItems()
{
if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
internal AppListItem[] GetPinnedApps()
{
BuildListItems();
}
return pinnedApps;
}
return allAppsSection;
public override IListItem[] GetItems()
{
// Build or update the list if needed
BuildListItems();
return pinnedApps.Concat(unpinnedApps).ToArray();
}
private void BuildListItems()
{
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload())
{
lock (_listLock)
{
this.IsLoading = true;
@@ -58,10 +74,9 @@ public sealed partial class AllAppsPage : ListPage
stopwatch.Start();
var apps = GetPrograms();
this.allAppsSection = apps
.Select((app) => new AppListItem(app, true))
.ToArray();
this.allApps = apps.AllApps;
this.pinnedApps = apps.PinnedItems;
this.unpinnedApps = apps.UnpinnedItems;
this.IsLoading = false;
@@ -70,56 +85,103 @@ public sealed partial class AllAppsPage : ListPage
stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
}
}
}
internal List<AppItem> GetPrograms()
private AppItem[] GetAllApps()
{
var uwpResults = AppCache.Instance.Value.UWPs
.Where((application) => application.Enabled)
.Select(UwpToAppItem);
.Select(app => app.ToAppItem());
var win32Results = AppCache.Instance.Value.Win32s
.Where((application) => application.Enabled && application.Valid)
.Select(app =>
{
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
app.IcoPath;
.Select(app => app.ToAppItem());
// icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = app.Type(),
IcoPath = icoPath,
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
DirPath = app.Location,
Commands = app.GetCommands(),
};
});
return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList();
var allApps = uwpResults.Concat(win32Results).ToArray();
return allApps;
}
private AppItem UwpToAppItem(UWPApplication app)
internal (AppItem[] AllApps, AppListItem[] PinnedItems, AppListItem[] UnpinnedItems) GetPrograms()
{
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
var item = new AppItem()
var allApps = GetAllApps();
var pinned = new List<AppListItem>();
var unpinned = new List<AppListItem>();
foreach (var app in allApps)
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = iconPath,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
};
return item;
var isPinned = PinnedAppsManager.Instance.IsAppPinned(app.AppIdentifier);
var appListItem = new AppListItem(app, true, isPinned);
if (isPinned)
{
appListItem.Tags = appListItem.Tags
.Concat([new Tag() { Icon = Icons.PinIcon }])
.ToArray();
pinned.Add(appListItem);
}
else
{
unpinned.Add(appListItem);
}
}
return (
allApps
.ToArray(),
pinned
.OrderBy(app => app.Title)
.ToArray(),
unpinned
.OrderBy(app => app.Title)
.ToArray());
}
private void OnPinStateChanged(object? sender, PinStateChangedEventArgs e)
{
/*
* Rebuilding all the lists is pretty expensive.
* So, instead, we'll just compare pinned items to move existing
* items between the two lists.
*/
var existingAppItem = allApps.FirstOrDefault(f => f.AppIdentifier == e.AppIdentifier);
if (existingAppItem != null)
{
var appListItem = new AppListItem(existingAppItem, true, e.IsPinned);
if (e.IsPinned)
{
// Remove it from the unpinned apps array
this.unpinnedApps = this.unpinnedApps
.Where(app => app.AppIdentifier != existingAppItem.AppIdentifier)
.OrderBy(app => app.Title)
.ToArray();
var newPinned = this.pinnedApps.ToList();
newPinned.Add(appListItem);
this.pinnedApps = newPinned
.OrderBy(app => app.Title)
.ToArray();
}
else
{
// Remove it from the pinned apps array
this.pinnedApps = this.pinnedApps
.Where(app => app.AppIdentifier != existingAppItem.AppIdentifier)
.OrderBy(app => app.Title)
.ToArray();
var newUnpinned = this.unpinnedApps.ToList();
newUnpinned.Add(appListItem);
this.unpinnedApps = newUnpinned
.OrderBy(app => app.Title)
.ToArray();
}
RaiseItemsChanged(0);
}
}
}

View File

@@ -4,6 +4,7 @@
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps;
@@ -26,7 +27,9 @@ internal sealed class AppItem
public bool IsPackaged { get; set; }
public List<CommandContextItem>? Commands { get; set; }
public List<IContextItem>? Commands { get; set; }
public string AppIdentifier { get; set; } = string.Empty;
public AppItem()
{

View File

@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.Storage.Streams;
@@ -23,14 +24,17 @@ internal sealed partial class AppListItem : ListItem
public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; }
public AppListItem(AppItem app, bool useThumbnails)
public string AppIdentifier => _app.AppIdentifier;
public AppListItem(AppItem app, bool useThumbnails, bool isPinned)
: base(new AppCommand(app))
{
_app = app;
Title = app.Name;
Subtitle = app.Subtitle;
Tags = [_appTag];
MoreCommands = _app.Commands!.ToArray();
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
_details = new Lazy<Details>(() =>
{
@@ -121,4 +125,37 @@ internal sealed partial class AppListItem : ListItem
return icon;
}
private IContextItem[] AddPinCommands(List<IContextItem> commands, bool isPinned)
{
var newCommands = new List<IContextItem>();
newCommands.AddRange(commands);
newCommands.Add(new SeparatorContextItem());
// 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,
});
}
else
{
newCommands.Add(
new CommandContextItem(
new PinAppCommand(this.AppIdentifier))
{
RequestedShortcut = pinKeyChord,
});
}
return newCommands.ToArray();
}
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Globalization;
using System.Text;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -13,14 +14,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class CopyPathCommand : InvokableCommand
{
private static readonly IconInfo TheIcon = new("\ue8c8");
private readonly string _target;
public CopyPathCommand(string target)
{
Name = Resources.copy_path;
Icon = TheIcon;
Icon = Icons.CopyIcon;
_target = target;
}

View File

@@ -6,6 +6,7 @@ using System;
using System.Diagnostics;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -13,14 +14,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class OpenInConsoleCommand : InvokableCommand
{
private static readonly IconInfo TheIcon = new("\ue838");
private readonly string _target;
public OpenInConsoleCommand(string target)
{
Name = Resources.open_path_in_console;
Icon = TheIcon;
Icon = Icons.OpenConsoleIcon;
_target = target;
}

View File

@@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -11,14 +12,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class OpenPathCommand : InvokableCommand
{
private static readonly IconInfo TheIcon = new("\ue838");
private readonly string _target;
public OpenPathCommand(string target)
{
Name = Resources.open_location;
Icon = TheIcon;
Icon = Icons.OpenPathIcon;
_target = target;
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Diagnostics.CodeAnalysis;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class PinAppCommand : InvokableCommand
{
private readonly string _appIdentifier;
public PinAppCommand(string appIdentifier)
{
_appIdentifier = appIdentifier;
Name = Resources.pin_app;
Icon = Icons.PinIcon;
}
public override CommandResult Invoke()
{
PinnedAppsManager.Instance.PinApp(_appIdentifier);
return CommandResult.KeepOpen();
}
}

View File

@@ -5,6 +5,7 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -13,8 +14,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class RunAsAdminCommand : InvokableCommand
{
private static readonly IconInfo TheIcon = new("\uE7EF");
private readonly string _target;
private readonly string _parentDir;
private readonly bool _packaged;
@@ -22,7 +21,7 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand
public RunAsAdminCommand(string target, string parentDir, bool packaged)
{
Name = Resources.run_as_administrator;
Icon = TheIcon;
Icon = Icons.RunAsIcon;
_target = target;
_parentDir = parentDir;

View File

@@ -5,6 +5,7 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions.Toolkit;
@@ -13,21 +14,19 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class RunAsUserCommand : InvokableCommand
{
private static readonly IconInfo TheIcon = new("\uE7EE");
private readonly string _target;
private readonly string _parentDir;
public RunAsUserCommand(string target, string parentDir)
{
Name = Resources.run_as_different_user;
Icon = TheIcon;
Icon = Icons.RunAsUserIcon;
_target = target;
_parentDir = parentDir;
}
internal static async Task RunAsAdmin(string target, string parentDir)
internal static async Task RunAsUser(string target, string parentDir)
{
await Task.Run(() =>
{
@@ -39,7 +38,7 @@ internal sealed partial class RunAsUserCommand : InvokableCommand
public override CommandResult Invoke()
{
_ = RunAsAdmin(_target, _parentDir).ConfigureAwait(false);
_ = RunAsUser(_target, _parentDir).ConfigureAwait(false);
return CommandResult.Dismiss();
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Commands;
internal sealed partial class UnpinAppCommand : InvokableCommand
{
private readonly string _appIdentifier;
public UnpinAppCommand(string appIdentifier)
{
_appIdentifier = appIdentifier;
Name = Resources.unpin_app;
Icon = Icons.UnpinIcon;
}
public override CommandResult Invoke()
{
PinnedAppsManager.Instance.UnpinApp(_appIdentifier);
return CommandResult.KeepOpen();
}
}

View File

@@ -0,0 +1,26 @@
// 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.Apps.Helpers;
public static partial class Icons
{
public static IconInfo UnpinIcon { get; } = new("\uE77A");
public static IconInfo PinIcon { get; } = new("\uE840");
public static IconInfo RunAsIcon { get; } = new("\uE7EF");
public static IconInfo RunAsUserIcon { get; } = new("\uE7EE");
public static IconInfo CopyIcon { get; } = new("\ue8c8");
public static IconInfo OpenConsoleIcon { get; } = new("\ue838");
public static IconInfo OpenPathIcon { get; } = new("\ue838");
public static IconInfo AllAppsIcon { get; } = IconHelpers.FromRelativePath("Assets\\AllApps.svg");
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.State;
namespace Microsoft.CmdPal.Ext.Apps;
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(PinnedApps))]
[JsonSerializable(typeof(List<string>), TypeInfoPropertyName = "StringList")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
internal sealed partial class JsonSerializationContext : JsonSerializerContext
{
}

View File

@@ -10,7 +10,9 @@ using System.Xml;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
using Windows.Win32;
@@ -70,9 +72,15 @@ public class UWPApplication : IProgram
return Resources.packaged_application;
}
public List<CommandContextItem> GetCommands()
public string GetAppIdentifier()
{
List<CommandContextItem> commands = [];
// Use UserModelId for UWP apps as it's unique
return UserModelId;
}
public List<IContextItem> GetCommands()
{
List<IContextItem> commands = [];
if (CanRunElevated)
{
@@ -511,6 +519,25 @@ public class UWPApplication : IProgram
}
}
internal AppItem ToAppItem()
{
var app = this;
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
var item = new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = UWPApplication.Type(),
IcoPath = iconPath,
DirPath = app.Location,
UserModelId = app.UserModelId,
IsPackaged = true,
Commands = app.GetCommands(),
AppIdentifier = app.GetAppIdentifier(),
};
return item;
}
/*
public ImageSource Logo()
{

View File

@@ -19,7 +19,9 @@ using System.Windows.Input;
using ManagedCommon;
using Microsoft.CmdPal.Ext.Apps.Commands;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.State;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.Win32;
using Windows.System;
@@ -186,9 +188,9 @@ public class Win32Program : IProgram
return true;
}
public List<CommandContextItem> GetCommands()
public List<IContextItem> GetCommands()
{
List<CommandContextItem> commands = new List<CommandContextItem>();
List<IContextItem> commands = new List<IContextItem>();
if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile)
{
@@ -231,6 +233,12 @@ public class Win32Program : IProgram
return ExecutableName;
}
public string GetAppIdentifier()
{
// Use a combination of name and path to create a unique identifier
return $"{Name}|{FullPath}";
}
private static Win32Program CreateWin32Program(string path)
{
try
@@ -933,4 +941,29 @@ public class Win32Program : IProgram
return Array.Empty<Win32Program>();
}
}
internal AppItem ToAppItem()
{
var app = this;
var icoPath = string.IsNullOrEmpty(app.IcoPath) ?
(app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ?
app.IcoPath :
app.FullPath) :
app.IcoPath;
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
app.FullPath :
icoPath;
return new AppItem()
{
Name = app.Name,
Subtitle = app.Description,
Type = app.Type(),
IcoPath = icoPath,
ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath,
DirPath = app.Location,
Commands = app.GetCommands(),
AppIdentifier = app.GetAppIdentifier(),
};
}
}

View File

@@ -213,6 +213,15 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Pin.
/// </summary>
internal static string pin_app {
get {
return ResourceManager.GetString("pin_app", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Run as administrator.
/// </summary>
@@ -267,6 +276,15 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties {
}
}
/// <summary>
/// Looks up a localized string similar to Unpin.
/// </summary>
internal static string unpin_app {
get {
return ResourceManager.GetString("unpin_app", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Experimental: When enabled, Command Palette will load thumbnails from the Windows Shell. Using thumbnails may cause the app to crash on launch.
/// </summary>

View File

@@ -199,4 +199,10 @@
<value>Experimental: When enabled, Command Palette will load thumbnails from the Windows Shell. Using thumbnails may cause the app to crash on launch</value>
<comment>A description for "use_thumbnails_setting_label"</comment>
</data>
<data name="pin_app" xml:space="preserve">
<value>Pin</value>
</data>
<data name="unpin_app" xml:space="preserve">
<value>Unpin</value>
</data>
</root>

View File

@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Ext.Apps.State;
public class PinStateChangedEventArgs : EventArgs
{
/// <summary>
/// Gets the identifier of the application whose pin state has changed.
/// </summary>
public string AppIdentifier { get; }
/// <summary>
/// Gets a value indicating whether the specified app identifier was pinned or not.
/// </summary>
public bool IsPinned { get; }
/// <summary>
/// Initializes a new instance of the <see cref="PinStateChangedEventArgs"/> class.
/// </summary>
/// <param name="appIdentifier">The identifier of the application whose pin state has changed.</param>
public PinStateChangedEventArgs(string appIdentifier, bool isPinned)
{
AppIdentifier = appIdentifier ?? throw new ArgumentNullException(nameof(appIdentifier));
IsPinned = isPinned;
}
}

View File

@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text.Json;
namespace Microsoft.CmdPal.Ext.Apps.State;
public sealed class PinnedApps
{
public List<string> PinnedAppIdentifiers { get; set; } = [];
public static PinnedApps ReadFromFile(string path)
{
if (!File.Exists(path))
{
return new PinnedApps();
}
try
{
var jsonString = File.ReadAllText(path);
var result = JsonSerializer.Deserialize<PinnedApps>(jsonString, JsonSerializationContext.Default.PinnedApps);
return result ?? new PinnedApps();
}
catch
{
return new PinnedApps();
}
}
public static void WriteToFile(string path, PinnedApps data)
{
try
{
var jsonString = JsonSerializer.Serialize(data, JsonSerializationContext.Default.PinnedApps);
File.WriteAllText(path, jsonString);
}
catch
{
// Silently fail - we don't want pinning issues to crash the extension
}
}
}

View File

@@ -0,0 +1,82 @@
// 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 System.IO;
using System.Linq;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.State;
public sealed class PinnedAppsManager
{
private static readonly Lazy<PinnedAppsManager> _instance = new(() => new PinnedAppsManager());
private readonly string _pinnedAppsFilePath;
public static PinnedAppsManager Instance => _instance.Value;
private PinnedApps _pinnedApps = new();
// Add event for when pinning state changes
public event EventHandler<PinStateChangedEventArgs>? PinStateChanged;
private PinnedAppsManager()
{
_pinnedAppsFilePath = GetPinnedAppsFilePath();
LoadPinnedApps();
}
public bool IsAppPinned(string appIdentifier)
{
return _pinnedApps.PinnedAppIdentifiers.Contains(appIdentifier, StringComparer.OrdinalIgnoreCase);
}
public void PinApp(string appIdentifier)
{
if (!IsAppPinned(appIdentifier))
{
_pinnedApps.PinnedAppIdentifiers.Add(appIdentifier);
SavePinnedApps();
Logger.LogTrace($"Pinned app: {appIdentifier}");
PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, true));
}
}
public string[] GetPinnedAppIdentifiers()
{
return _pinnedApps.PinnedAppIdentifiers.ToArray();
}
public void UnpinApp(string appIdentifier)
{
var removed = _pinnedApps.PinnedAppIdentifiers.RemoveAll(id =>
string.Equals(id, appIdentifier, StringComparison.OrdinalIgnoreCase));
if (removed > 0)
{
SavePinnedApps();
Logger.LogTrace($"Unpinned app: {appIdentifier}");
PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, false));
}
}
private void LoadPinnedApps()
{
_pinnedApps = PinnedApps.ReadFromFile(_pinnedAppsFilePath);
}
private void SavePinnedApps()
{
PinnedApps.WriteToFile(_pinnedAppsFilePath, _pinnedApps);
}
private static string GetPinnedAppsFilePath()
{
var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal");
Directory.CreateDirectory(directory);
return Path.Combine(directory, "apps.pinned.json");
}
}