workspace is done

This commit is contained in:
kaitao-ms
2025-11-28 17:12:25 +08:00
parent 883b9d6adc
commit 45ee64b644
19 changed files with 379 additions and 840 deletions

View File

@@ -2,7 +2,6 @@
// 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 PowerToys.ModuleContracts;
using WorkspacesCsharpLibrary.Data;
@@ -19,7 +18,5 @@ public interface IWorkspaceService : IModuleService
Task<OperationResult> SnapshotAsync(string? targetPath = null, CancellationToken cancellationToken = default);
[RequiresUnreferencedCode("Workspace deserialization uses reflection-based JSON serializer.")]
[RequiresDynamicCode("Workspace deserialization uses reflection-based JSON serializer.")]
Task<OperationResult<IReadOnlyList<ProjectWrapper>>> GetWorkspacesAsync(CancellationToken cancellationToken = default);
}

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Common.UI;
using ManagedCommon;
using PowerToys.Interop;
@@ -80,8 +80,6 @@ public sealed class WorkspaceService : ModuleServiceBase, IWorkspaceService
return Task.FromResult(OperationResult.Fail("Snapshot is not implemented for Workspaces."));
}
[RequiresUnreferencedCode("Workspace deserialization uses reflection-based JSON serializer.")]
[RequiresDynamicCode("Workspace deserialization uses reflection-based JSON serializer.")]
public Task<OperationResult<IReadOnlyList<ProjectWrapper>>> GetWorkspacesAsync(CancellationToken cancellationToken = default)
{
try

View File

@@ -4,11 +4,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Text.Json;
using WorkspacesCsharpLibrary.Data;
namespace WorkspacesCsharpLibrary.Data;
@@ -17,13 +15,6 @@ namespace WorkspacesCsharpLibrary.Data;
/// </summary>
public static class WorkspacesStorage
{
private static readonly JsonSerializerOptions _loadOptions = new()
{
PropertyNameCaseInsensitive = true,
};
[RequiresUnreferencedCode("Workspace file deserialization uses reflection-based JSON serializer.")]
[RequiresDynamicCode("Workspace file deserialization uses reflection-based JSON serializer.")]
public static IReadOnlyList<ProjectWrapper> Load()
{
var filePath = GetDefaultFilePath();
@@ -35,7 +26,7 @@ public static class WorkspacesStorage
try
{
var json = File.ReadAllText(filePath);
var data = JsonSerializer.Deserialize<WorkspacesFile>(json, _loadOptions);
var data = JsonSerializer.Deserialize(json, WorkspacesStorageJsonContext.Default.WorkspacesFile);
if (data?.Workspaces == null)
{
@@ -70,12 +61,12 @@ public static class WorkspacesStorage
return Path.Combine(localAppData, "Microsoft", "PowerToys", "Workspaces", "workspaces.json");
}
private sealed class WorkspacesFile
internal sealed class WorkspacesFile
{
public List<WorkspaceProject> Workspaces { get; set; } = new();
}
private sealed class WorkspaceProject
internal sealed class WorkspaceProject
{
public string Id { get; set; } = string.Empty;

View File

@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Text.Json.Serialization;
namespace WorkspacesCsharpLibrary.Data;
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
[JsonSerializable(typeof(WorkspacesStorage.WorkspacesFile))]
internal sealed partial class WorkspacesStorageJsonContext : JsonSerializerContext
{
}

View File

@@ -2,10 +2,8 @@
// 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;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using Workspaces.ModuleServices;
namespace PowerToysExtension.Commands;
@@ -25,25 +23,12 @@ internal sealed partial class LaunchWorkspaceCommand : InvokableCommand
return CommandResult.KeepOpen();
}
try
var result = WorkspaceService.Instance.LaunchWorkspaceAsync(_workspaceId).GetAwaiter().GetResult();
if (!result.Success)
{
var launcherPath = PowerToysPathResolver.TryResolveExecutable("PowerToys.WorkspacesLauncher.exe");
if (string.IsNullOrEmpty(launcherPath))
{
return CommandResult.ShowToast("Unable to locate PowerToys Workspaces launcher.");
return CommandResult.ShowToast(result.Error ?? "Launching workspace failed.");
}
var startInfo = new ProcessStartInfo(launcherPath, _workspaceId)
{
UseShellExecute = true,
};
Process.Start(startInfo);
return CommandResult.Hide();
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Launching workspace failed: {ex.Message}");
}
}
}

View File

@@ -2,102 +2,21 @@
// 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;
using System.Threading;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using Workspaces.ModuleServices;
namespace PowerToysExtension.Commands;
internal sealed partial class OpenWorkspaceEditorCommand : InvokableCommand
{
private const string LaunchEditorEventName = "Local\\Workspaces-LaunchEditorEvent-a55ff427-cf62-4994-a2cd-9f72139296bf";
public override CommandResult Invoke()
{
try
{
if (TrySignalLaunchEvent())
var result = WorkspaceService.Instance.LaunchEditorAsync().GetAwaiter().GetResult();
if (!result.Success)
{
return CommandResult.ShowToast(result.Error ?? "Unable to launch the Workspaces editor.");
}
return CommandResult.Hide();
}
if (TryLaunchEditorExecutable())
{
return CommandResult.Hide();
}
if (TryLaunchThroughSettings())
{
return CommandResult.Hide();
}
return CommandResult.ShowToast("Unable to launch the Workspaces editor.");
}
catch (Exception ex)
{
return CommandResult.ShowToast($"Launching editor failed: {ex.Message}");
}
}
private static bool TrySignalLaunchEvent()
{
try
{
using var existing = EventWaitHandle.OpenExisting(LaunchEditorEventName);
return existing.Set();
}
catch (WaitHandleCannotBeOpenedException)
{
try
{
using var created = new EventWaitHandle(false, EventResetMode.AutoReset, LaunchEditorEventName, out _);
return created.Set();
}
catch
{
return false;
}
}
catch
{
return false;
}
}
private static bool TryLaunchEditorExecutable()
{
var editorPath = PowerToysPathResolver.TryResolveExecutable("PowerToys.WorkspacesEditor.exe");
if (string.IsNullOrEmpty(editorPath))
{
return false;
}
var startInfo = new ProcessStartInfo(editorPath)
{
UseShellExecute = true,
};
Process.Start(startInfo);
return true;
}
private static bool TryLaunchThroughSettings()
{
var powerToysExe = PowerToysPathResolver.TryResolveExecutable("PowerToys.exe");
if (string.IsNullOrEmpty(powerToysExe))
{
return false;
}
var startInfo = new ProcessStartInfo(powerToysExe)
{
Arguments = "--open-settings=Workspaces",
UseShellExecute = false,
};
Process.Start(startInfo);
return true;
}
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Common.Search.FuzzSearch;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Modules;
namespace PowerToysExtension.Helpers;
/// <summary>
/// Aggregates commands exposed by individual module providers and applies fuzzy filtering.
/// </summary>
internal static class ModuleCommandCatalog
{
private static readonly ModuleCommandProvider[] Providers =
[
new AwakeModuleCommandProvider(),
new WorkspacesModuleCommandProvider(),
new ColorPickerModuleCommandProvider(),
new DefaultSettingsModuleCommandProvider(),
];
public static IListItem[] FilteredItems(string query)
{
var all = Providers.SelectMany(provider => provider.BuildCommands()).ToList();
if (string.IsNullOrWhiteSpace(query))
{
return [.. all];
}
var matched = new List<Tuple<int, ListItem>>();
foreach (var item in all)
{
var result = StringMatcher.FuzzyMatch(query, item.Title);
if (result.Success)
{
matched.Add(new Tuple<int, ListItem>(result.Score, item));
}
}
matched.Sort((x, y) => y.Item1.CompareTo(x.Item1));
return [.. matched.Select(x => x.Item2)];
}
}

View File

@@ -1,113 +0,0 @@
// 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 Common.Search.FuzzSearch;
using Common.UI;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Pages;
namespace PowerToysExtension.Helpers;
/// <summary>
/// Builds the list of PowerToys module entries and supports basic fuzzy filtering.
/// </summary>
internal static class ModuleItemsHelper
{
private static List<ListItem>? _cache;
public static IListItem[] FilteredItems(string query)
{
var all = AllItems();
if (string.IsNullOrWhiteSpace(query))
{
return [.. all];
}
var matched = new List<Tuple<int, ListItem>>();
foreach (var item in all)
{
var result = StringMatcher.FuzzyMatch(query, item.Title);
if (result.Success)
{
matched.Add(new Tuple<int, ListItem>(result.Score, item));
}
}
matched.Sort((x, y) => y.Item1.CompareTo(x.Item1));
return [.. matched.Select(x => x.Item2)];
}
private static List<ListItem> AllItems()
{
if (_cache is not null)
{
return _cache;
}
var items = new List<ListItem>();
foreach (var module in Enum.GetValues<SettingsDeepLink.SettingsWindow>())
{
var item = CreateItem(module);
if (item is not null)
{
items.Add(item);
}
}
_cache = items;
return items;
}
private static ListItem? CreateItem(SettingsDeepLink.SettingsWindow module)
{
// Skip purely internal pages.
if (module is SettingsDeepLink.SettingsWindow.Dashboard)
{
return null;
}
var icon = module.ModuleIcon();
var title = module.ModuleDisplayName();
var settingsCommand = new OpenInSettingsCommand(module, title);
var more = new List<ICommandContextItem>();
switch (module)
{
case SettingsDeepLink.SettingsWindow.Awake:
more.Add(new CommandContextItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => "-m indefinite", "Awake set to indefinite")));
more.Add(new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => "-m timed -t 30", "Awake set for 30 minutes")));
more.Add(new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => "-m timed -t 120", "Awake set for 2 hours")));
more.Add(new CommandContextItem(new StopAwakeCommand()));
break;
case SettingsDeepLink.SettingsWindow.Workspaces:
more.Add(new CommandContextItem(new WorkspacesListPage()));
more.Add(new CommandContextItem(new OpenWorkspaceEditorCommand()));
break;
case SettingsDeepLink.SettingsWindow.ColorPicker:
more.Add(new CommandContextItem(new CopyColorCommand()));
break;
default:
break;
}
var command = new CommandItem(settingsCommand)
{
Title = title,
Icon = icon,
MoreCommands = more.Count > 0 ? [.. more] : [],
};
return new ListItem(command);
}
}

View File

@@ -6,9 +6,6 @@ using System.Text.Json.Serialization;
namespace PowerToysExtension.Helpers;
[JsonSerializable(typeof(WorkspaceItemsHelper.WorkspacesData))]
[JsonSerializable(typeof(WorkspaceItemsHelper.WorkspaceProject))]
[JsonSerializable(typeof(WorkspaceItemsHelper.WorkspaceApplication))]
[JsonSerializable(typeof(AwakeSettingsDocument))]
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, PropertyNameCaseInsensitive = true)]
internal sealed partial class PowerToysJsonContext : JsonSerializerContext

View File

@@ -1,452 +0,0 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using NoOpCommand = PowerToysExtension.Commands.NoOpCommand;
namespace Microsoft.CmdPal.Ext.PowerToys.Helpers;
internal static class PowerToysModuleItemsHelper
{
private sealed record ModuleMetadata(
string SettingsKey,
string Title,
string IconPath,
string[] SearchTerms,
string? Subtitle = null,
string? LaunchEvent = null,
string? LaunchExecutable = null,
string? LaunchArguments = null,
ModuleCommandMetadata[]? AdditionalLaunchCommands = null);
private sealed record ModuleCommandMetadata(
string Title,
string? Subtitle = null,
string? LaunchEvent = null,
string? LaunchExecutable = null,
string? LaunchArguments = null,
string[]? SearchTerms = null);
private sealed record CustomEntry(Func<ListItem> Factory, string[] SearchTerms)
{
public ListItem CreateItem() => Factory();
}
private static readonly ModuleMetadata[] Modules =
[
new(
SettingsKey: "AdvancedPaste",
Title: "Advanced Paste",
IconPath: "Assets\\AdvancedPaste.png",
SearchTerms: new[] { "advanced", "paste", "clipboard", "text" },
Subtitle: "Open Advanced Paste settings"),
new(
SettingsKey: "AlwaysOnTop",
Title: "Always On Top",
IconPath: "Assets\\AlwaysOnTop.png",
SearchTerms: new[] { "always", "top", "pin", "window" },
Subtitle: "Toggle Always On Top for the active window"),
new(
SettingsKey: "Awake",
Title: "Awake",
IconPath: "Assets\\Awake.png",
SearchTerms: new[] { "awake", "sleep", "keep", "monitor" },
Subtitle: "Keep your PC awake",
LaunchExecutable: "PowerToys.Awake.exe",
LaunchArguments: "--use-parent-pid --display-on true"),
new(
SettingsKey: "ColorPicker",
Title: "Color Picker",
IconPath: "Assets\\ColorPicker.png",
SearchTerms: new[] { "color", "picker", "eyedropper", "pick" },
Subtitle: "Copy colors from anywhere",
LaunchEvent: PowerToysEventNames.ColorPickerShow,
LaunchExecutable: "PowerToys.ColorPickerUI.exe"),
new(
SettingsKey: "CropAndLock",
Title: "Crop And Lock",
IconPath: "Assets\\CropAndLock.png",
SearchTerms: new[] { "crop", "lock", "thumbnail", "reparent" },
Subtitle: "Configure Crop And Lock",
AdditionalLaunchCommands: new[]
{
new ModuleCommandMetadata(
Title: "Crop active window (thumbnail)",
Subtitle: "Creates a static snapshot window",
LaunchEvent: PowerToysEventNames.CropAndLockThumbnail,
SearchTerms: new[] { "thumbnail", "snapshot", "crop" }),
new ModuleCommandMetadata(
Title: "Crop active window (reparent)",
Subtitle: "Embeds the active window inside a new host",
LaunchEvent: PowerToysEventNames.CropAndLockReparent,
SearchTerms: new[] { "reparent", "embed", "live window" }),
}),
new(
SettingsKey: "EnvironmentVariables",
Title: "Environment Variables",
IconPath: "Assets\\EnvironmentVariables.png",
SearchTerms: new[] { "environment", "variables", "env", "path" },
Subtitle: "Manage environment variables",
LaunchEvent: PowerToysEventNames.EnvironmentVariablesShow,
AdditionalLaunchCommands: new[]
{
new ModuleCommandMetadata(
Title: "Open as administrator",
Subtitle: "Launches the elevated editor",
LaunchEvent: PowerToysEventNames.EnvironmentVariablesShowAdmin,
SearchTerms: new[] { "admin", "administrator" }),
}),
new(
SettingsKey: "FancyZones",
Title: "FancyZones",
IconPath: "Assets\\FancyZones.png",
SearchTerms: new[] { "fancyzones", "zones", "layout", "window" },
Subtitle: "Adjust FancyZones layouts",
LaunchEvent: PowerToysEventNames.FancyZonesToggleEditor,
LaunchExecutable: "PowerToys.FancyZonesEditor.exe"),
new(
SettingsKey: "FileExplorer",
Title: "File Explorer Add-ons",
IconPath: "Assets\\FileExplorerPreview.png",
SearchTerms: new[] { "file explorer", "preview", "addons", "powerpreview" },
Subtitle: "Configure File Explorer add-ons"),
new(
SettingsKey: "FileLocksmith",
Title: "File Locksmith",
IconPath: "Assets\\FileLocksmith.png",
SearchTerms: new[] { "file", "locksmith", "lock", "unlock" },
Subtitle: "Find which process locks a file"),
new(
SettingsKey: "Hosts",
Title: "Hosts File Editor",
IconPath: "Assets\\Hosts.png",
SearchTerms: new[] { "hosts", "file", "editor" },
Subtitle: "Edit the hosts file",
LaunchEvent: PowerToysEventNames.HostsShow,
AdditionalLaunchCommands: new[]
{
new ModuleCommandMetadata(
Title: "Open as administrator",
Subtitle: "Launches the elevated editor",
LaunchEvent: PowerToysEventNames.HostsShowAdmin,
SearchTerms: new[] { "admin", "administrator" }),
}),
new(
SettingsKey: "ImageResizer",
Title: "Image Resizer",
IconPath: "Assets\\ImageResizer.png",
SearchTerms: new[] { "image", "resize", "resizer", "photo" },
Subtitle: "Resize images in bulk"),
new(
SettingsKey: "KBM",
Title: "Keyboard Manager",
IconPath: "Assets\\KeyboardManager.png",
SearchTerms: new[] { "keyboard", "manager", "remap", "kbm", "shortcut" },
Subtitle: "Remap keys and shortcuts"),
new(
SettingsKey: "MeasureTool",
Title: "Screen Ruler",
IconPath: "Assets\\ScreenRuler.png",
SearchTerms: new[] { "screen", "ruler", "measure", "measuretool" },
Subtitle: "Measure on-screen elements",
LaunchEvent: PowerToysEventNames.MeasureToolTrigger),
new(
SettingsKey: "MouseUtils",
Title: "Mouse Utilities",
IconPath: "Assets\\MouseUtils.png",
SearchTerms: new[] { "mouse", "utilities", "finder", "highlighter", "crosshairs" },
Subtitle: "Configure mouse utilities"),
new(
SettingsKey: "MouseWithoutBorders",
Title: "Mouse Without Borders",
IconPath: "Assets\\MouseWithoutBorders.png",
SearchTerms: new[] { "mouse", "borders", "multi pc", "mwob" },
Subtitle: "Share mouse and keyboard across PCs"),
new(
SettingsKey: "NewPlus",
Title: "New+",
IconPath: "Assets\\NewPlus.png",
SearchTerms: new[] { "new", "template", "file" },
Subtitle: "Create templates quickly"),
new(
SettingsKey: "Peek",
Title: "Peek",
IconPath: "Assets\\Peek.png",
SearchTerms: new[] { "peek", "preview", "quick look" },
Subtitle: "Preview files instantly",
LaunchEvent: PowerToysEventNames.PeekShow),
new(
SettingsKey: "PowerAccent",
Title: "Quick Accent",
IconPath: "Assets\\QuickAccent.png",
SearchTerms: new[] { "accent", "quick", "characters", "diacritics", "poweraccent" },
Subtitle: "Insert accented characters"),
new(
SettingsKey: "PowerOCR",
Title: "Text Extractor",
IconPath: "Assets\\TextExtractor.png",
SearchTerms: new[] { "text", "extractor", "ocr", "copy" },
Subtitle: "Extract text from the screen",
LaunchEvent: PowerToysEventNames.PowerOcrShow,
LaunchExecutable: "PowerToys.PowerOCR.exe"),
new(
SettingsKey: "PowerRename",
Title: "PowerRename",
IconPath: "Assets\\PowerRename.png",
SearchTerms: new[] { "rename", "files", "powerrename" },
Subtitle: "Batch rename files",
LaunchExecutable: "PowerRename.exe"),
new(
SettingsKey: "RegistryPreview",
Title: "Registry Preview",
IconPath: "Assets\\RegistryPreview.png",
SearchTerms: new[] { "registry", "preview", "reg" },
Subtitle: "Inspect and edit registry files",
LaunchEvent: PowerToysEventNames.RegistryPreviewTrigger),
new(
SettingsKey: "Run",
Title: "PowerToys Run",
IconPath: "Assets\\PowerToysRun.png",
SearchTerms: new[] { "run", "launcher", "search", "powertoys run", "powerlauncher" },
Subtitle: "Quickly search and launch",
LaunchEvent: PowerToysEventNames.PowerToysRunInvoke),
new(
SettingsKey: "ShortcutGuide",
Title: "Shortcut Guide",
IconPath: "Assets\\ShortcutGuide.png",
SearchTerms: new[] { "shortcut", "guide", "keys", "help" },
Subtitle: "View available shortcuts",
LaunchEvent: PowerToysEventNames.ShortcutGuideTrigger),
new(
SettingsKey: "CmdPal",
Title: "Command Palette",
IconPath: "Assets\\CmdPal.png",
SearchTerms: new[] { "command", "palette", "cmdpal", "prompt" },
Subtitle: "Open the Command Palette",
LaunchEvent: PowerToysEventNames.CommandPaletteShow),
new(
SettingsKey: "CmdNotFound",
Title: "Command Not Found",
IconPath: "Assets\\CommandNotFound.png",
SearchTerms: new[] { "command", "not", "found", "terminal" },
Subtitle: "Suggest commands when mistyped"),
new(
SettingsKey: "Workspaces",
Title: "Workspaces",
IconPath: "Assets\\Workspaces.png",
SearchTerms: new[] { "workspace", "layouts", "projects" },
Subtitle: "Manage PowerToys workspaces",
LaunchEvent: PowerToysEventNames.WorkspacesLaunchEditor,
AdditionalLaunchCommands: new[]
{
new ModuleCommandMetadata(
Title: "Trigger Workspaces hotkey",
Subtitle: "Invokes the configured Workspaces action",
LaunchEvent: PowerToysEventNames.WorkspacesHotkey,
SearchTerms: new[] { "hotkey", "shortcut" }),
}),
new(
SettingsKey: "ZoomIt",
Title: "ZoomIt",
IconPath: "Assets\\ZoomIt.png",
SearchTerms: new[] { "zoom", "zoomit", "presentation" },
Subtitle: "Configure ZoomIt"),
new(
SettingsKey: "Overview",
Title: "General",
IconPath: "Assets\\PowerToys.png",
SearchTerms: new[] { "general", "overview", "about" },
Subtitle: "General PowerToys settings"),
new(
SettingsKey: "Dashboard",
Title: "Dashboard",
IconPath: "Assets\\PowerToys.png",
SearchTerms: new[] { "dashboard", "status", "summary" },
Subtitle: "View overall status"),
];
private static readonly CustomEntry[] CustomEntries =
[
new(
() => new ListItem(new CommandItem(new WorkspacesListPage()))
{
Title = "Workspaces list",
Subtitle = "Browse individual workspaces",
Icon = IconHelpers.FromRelativePath("Assets\\Workspaces.png"),
},
new[] { "workspace", "workspaces", "layout" }),
new(
() => new ListItem(new CommandItem(new AwakeSessionsPage()))
{
Title = "Awake actions",
Subtitle = "Start, stop, or schedule Awake",
Icon = IconHelpers.FromRelativePath("Assets\\Awake.png"),
},
new[] { "awake", "keep awake", "sleep", "prevent", "timer" }),
];
internal static IListItem[] GetModuleItems(string? searchText)
{
var results = new List<IListItem>();
foreach (var module in Modules)
{
if (Matches(module, searchText))
{
results.Add(CreateModuleItem(module));
}
}
foreach (var entry in CustomEntries)
{
var item = entry.CreateItem();
if (Matches(item, entry.SearchTerms, searchText))
{
results.Add(item);
}
}
var workspaceItems = WorkspaceItemsHelper.GetWorkspaceItems(searchText);
if (workspaceItems.Length > 0)
{
results.AddRange(workspaceItems);
}
return results.ToArray();
}
private static ListItem CreateModuleItem(ModuleMetadata metadata)
{
var mainCommand = CreatePrimaryCommand(metadata, out var usesSettingsForPrimary);
var listItem = new ListItem(mainCommand)
{
Title = metadata.Title,
Subtitle = metadata.Subtitle ?? (usesSettingsForPrimary ? "Open module settings" : $"Launch {metadata.Title}"),
Icon = IconHelpers.FromRelativePath(metadata.IconPath),
};
var moreCommands = new List<ICommandContextItem>();
if (string.Equals(metadata.SettingsKey, "Awake", StringComparison.OrdinalIgnoreCase))
{
listItem.Subtitle = AwakeStatusService.BuildSubtitle();
AwakeCommandsFactory.PopulateModuleCommands(moreCommands);
}
if (metadata.AdditionalLaunchCommands is { Length: > 0 })
{
foreach (var additional in metadata.AdditionalLaunchCommands)
{
var additionalCommand = new LaunchModuleCommand(
metadata.Title,
additional.LaunchEvent,
additional.LaunchExecutable,
additional.LaunchArguments,
additional.Title);
var contextItem = new CommandContextItem(additionalCommand);
if (!string.IsNullOrWhiteSpace(additional.Title))
{
contextItem.Title = additional.Title;
}
if (!string.IsNullOrWhiteSpace(additional.Subtitle))
{
contextItem.Subtitle = additional.Subtitle;
}
moreCommands.Add(contextItem);
}
}
if (!usesSettingsForPrimary && !string.IsNullOrEmpty(metadata.SettingsKey))
{
moreCommands.Add(new CommandContextItem(new OpenPowerToysSettingsCommand(metadata.Title, metadata.SettingsKey)));
}
if (moreCommands.Count > 0)
{
listItem.MoreCommands = moreCommands.ToArray();
}
return listItem;
}
private static InvokableCommand CreatePrimaryCommand(ModuleMetadata metadata, out bool usesSettings)
{
usesSettings = false;
if (!string.IsNullOrEmpty(metadata.LaunchEvent) || !string.IsNullOrEmpty(metadata.LaunchExecutable))
{
return new LaunchModuleCommand(metadata.Title, metadata.LaunchEvent, metadata.LaunchExecutable, metadata.LaunchArguments);
}
if (!string.IsNullOrEmpty(metadata.SettingsKey))
{
usesSettings = true;
return new OpenPowerToysSettingsCommand(metadata.Title, metadata.SettingsKey);
}
return new NoOpCommand();
}
private static bool Matches(ModuleMetadata metadata, string? searchText)
{
if (string.IsNullOrWhiteSpace(searchText))
{
return true;
}
if (Contains(metadata.Title, searchText) || Contains(metadata.Subtitle, searchText))
{
return true;
}
if (metadata.AdditionalLaunchCommands is { Length: > 0 })
{
foreach (var additional in metadata.AdditionalLaunchCommands)
{
if (Contains(additional.Title, searchText) || Contains(additional.Subtitle, searchText))
{
return true;
}
if (additional.SearchTerms is { Length: > 0 } && additional.SearchTerms.Any(term => Contains(term, searchText)))
{
return true;
}
}
}
return metadata.SearchTerms.Any(term => Contains(term, searchText));
}
private static bool Matches(ListItem item, IReadOnlyCollection<string> searchTerms, string? searchText)
{
if (string.IsNullOrWhiteSpace(searchText))
{
return true;
}
if (Contains(item.Title, searchText) || Contains(item.Subtitle, searchText))
{
return true;
}
return searchTerms.Any(term => Contains(term, searchText));
}
private static bool Contains(string? source, string query)
{
return !string.IsNullOrEmpty(source) && source.Contains(query, StringComparison.CurrentCultureIgnoreCase);
}
}

View File

@@ -1,144 +0,0 @@
// 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.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
namespace PowerToysExtension.Helpers;
internal static class WorkspaceItemsHelper
{
private const string WorkspacesDirectory = "Microsoft\\PowerToys\\Workspaces";
private const string WorkspacesFileName = "workspaces.json";
internal sealed class WorkspaceApplication
{
public string? Application { get; set; }
}
internal sealed class WorkspaceProject
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public List<WorkspaceApplication>? Applications { get; set; }
}
internal sealed class WorkspacesData
{
public List<WorkspaceProject>? Workspaces { get; set; }
}
internal static ListItem CreateOpenEditorItem()
{
return new ListItem(new OpenWorkspaceEditorCommand())
{
Title = "Open Workspaces editor",
Subtitle = "Launch the PowerToys Workspaces editor",
Icon = IconHelpers.FromRelativePath("Assets\\Workspaces.png"),
};
}
internal static IListItem[] GetWorkspaceItems(string? searchText)
{
var workspaces = LoadWorkspaces();
var filtered = string.IsNullOrWhiteSpace(searchText)
? workspaces
: workspaces.Where(ws => Contains(ws.Name, searchText)).ToList();
var items = new List<IListItem>(filtered.Count + 1)
{
CreateOpenEditorItem(),
};
foreach (var workspace in filtered)
{
if (string.IsNullOrEmpty(workspace.Id) || string.IsNullOrEmpty(workspace.Name))
{
continue;
}
items.Add(new ListItem(new LaunchWorkspaceCommand(workspace.Id))
{
Title = workspace.Name,
Subtitle = BuildSubtitle(workspace),
Icon = IconHelpers.FromRelativePath("Assets\\Workspaces.png"),
});
}
return items.ToArray();
}
internal static IListItem[] FilteredItems(string searchText) => GetWorkspaceItems(searchText);
private static List<WorkspaceProject> LoadWorkspaces()
{
try
{
var path = GetWorkspacesFilePath();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return [];
}
var json = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(json))
{
return [];
}
var data = JsonSerializer.Deserialize(json, PowerToysJsonContext.Default.WorkspacesData);
return data?.Workspaces ?? [];
}
catch
{
return [];
}
}
private static string? GetWorkspacesFilePath()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
if (string.IsNullOrEmpty(localAppData))
{
return null;
}
return Path.Combine(localAppData, WorkspacesDirectory, WorkspacesFileName);
}
private static string BuildSubtitle(WorkspaceProject workspace)
{
var count = workspace.Applications?.Count ?? 0;
if (count == 0)
{
return "No applications";
}
if (count == 1)
{
return "1 application";
}
return string.Format(CultureInfo.CurrentCulture, "{0} applications", count);
}
private static bool Contains(string? source, string needle)
{
if (string.IsNullOrEmpty(source))
{
return false;
}
return source.Contains(needle, StringComparison.CurrentCultureIgnoreCase);
}
}

View File

@@ -56,6 +56,7 @@
<ProjectReference Include="..\..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.Search\Common.Search.csproj" />
<ProjectReference Include="..\..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\Workspaces\Workspaces.ModuleServices\Workspaces.ModuleServices.csproj" />
</ItemGroup>
<!-- Exclude legacy Helper folder in favor of Helpers -->

View File

@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Modules;
internal sealed class AwakeModuleCommandProvider : ModuleCommandProvider
{
public override IEnumerable<ListItem> BuildCommands()
{
var title = SettingsDeepLink.SettingsWindow.Awake.ModuleDisplayName();
var icon = SettingsDeepLink.SettingsWindow.Awake.ModuleIcon();
var more = new List<CommandContextItem>
{
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake indefinitely", () => "-m indefinite", "Awake set to indefinite")),
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 30 minutes", () => "-m timed -t 30", "Awake set for 30 minutes")),
new CommandContextItem(new StartAwakeCommand("Awake: Keep awake for 2 hours", () => "-m timed -t 120", "Awake set for 2 hours")),
new CommandContextItem(new StopAwakeCommand()),
};
var item = new ListItem(new OpenInSettingsCommand(SettingsDeepLink.SettingsWindow.Awake, title))
{
Title = title,
Subtitle = "Open Awake settings",
Icon = icon,
MoreCommands = more.ToArray(),
};
return [item];
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Modules;
internal sealed class ColorPickerModuleCommandProvider : ModuleCommandProvider
{
public override IEnumerable<ListItem> BuildCommands()
{
var title = SettingsDeepLink.SettingsWindow.ColorPicker.ModuleDisplayName();
var icon = SettingsDeepLink.SettingsWindow.ColorPicker.ModuleIcon();
var more = new List<CommandContextItem>
{
new CommandContextItem(new CopyColorCommand()),
};
var item = new ListItem(new OpenInSettingsCommand(SettingsDeepLink.SettingsWindow.ColorPicker, title))
{
Title = title,
Subtitle = "Open Color Picker settings",
Icon = icon,
MoreCommands = more.ToArray(),
};
return [item];
}
}

View File

@@ -0,0 +1,46 @@
// 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 Common.UI;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
namespace PowerToysExtension.Modules;
/// <summary>
/// Provides open-settings commands for modules without specialized commands.
/// </summary>
internal sealed class DefaultSettingsModuleCommandProvider : ModuleCommandProvider
{
private static readonly SettingsDeepLink.SettingsWindow[] _excluded =
[
SettingsDeepLink.SettingsWindow.Dashboard,
SettingsDeepLink.SettingsWindow.Workspaces,
SettingsDeepLink.SettingsWindow.Awake,
SettingsDeepLink.SettingsWindow.ColorPicker,
];
public override IEnumerable<ListItem> BuildCommands()
{
foreach (var module in Enum.GetValues<SettingsDeepLink.SettingsWindow>())
{
if (_excluded.Contains(module))
{
continue;
}
var title = module.ModuleDisplayName();
yield return new ListItem(new OpenInSettingsCommand(module, title))
{
Title = title,
Subtitle = "Open module settings",
Icon = module.ModuleIcon(),
};
}
}
}

View File

@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace PowerToysExtension.Modules;
/// <summary>
/// Base contract for a PowerToys module to expose its command palette entries.
/// </summary>
internal abstract class ModuleCommandProvider
{
public abstract IEnumerable<ListItem> BuildCommands();
}

View File

@@ -0,0 +1,162 @@
// 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 Common.UI;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Commands;
using PowerToysExtension.Helpers;
using PowerToysExtension.Pages;
using Workspaces.ModuleServices;
using WorkspacesCsharpLibrary.Data;
namespace PowerToysExtension.Modules;
internal sealed class WorkspacesModuleCommandProvider : ModuleCommandProvider
{
public override IEnumerable<ListItem> BuildCommands()
{
var items = new List<ListItem>();
// Settings entry plus common actions.
var title = SettingsDeepLink.SettingsWindow.Workspaces.ModuleDisplayName();
items.Add(new ListItem(new OpenInSettingsCommand(SettingsDeepLink.SettingsWindow.Workspaces, title))
{
Title = title,
Subtitle = "Open Workspaces settings",
Icon = SettingsDeepLink.SettingsWindow.Workspaces.ModuleIcon(),
MoreCommands =
[
new CommandContextItem(new WorkspacesListPage()),
new CommandContextItem(new OpenWorkspaceEditorCommand()),
],
});
// Per-workspace entries via the shared service.
foreach (var workspace in LoadWorkspaces())
{
if (string.IsNullOrWhiteSpace(workspace.Id) || string.IsNullOrWhiteSpace(workspace.Name))
{
continue;
}
items.Add(new ListItem(new LaunchWorkspaceCommand(workspace.Id))
{
Title = workspace.Name,
Subtitle = BuildSubtitle(workspace),
Icon = IconHelpers.FromRelativePath("Assets\\Workspaces.png"),
Details = BuildDetails(workspace),
});
}
return items;
}
private static IReadOnlyList<ProjectWrapper> LoadWorkspaces()
{
var result = WorkspaceService.Instance.GetWorkspacesAsync().GetAwaiter().GetResult();
return result.Success && result.Value is not null ? result.Value : System.Array.Empty<ProjectWrapper>();
}
private static string BuildSubtitle(ProjectWrapper workspace)
{
var appCount = workspace.Applications?.Count ?? 0;
var monitorCount = workspace.MonitorConfiguration?.Count ?? 0;
var appsText = appCount switch
{
0 => "No applications",
1 => "1 application",
_ => string.Format(CultureInfo.CurrentCulture, "{0} applications", appCount),
};
var monitorsText = monitorCount switch
{
0 => "No monitors",
1 => "1 monitor",
_ => string.Format(CultureInfo.CurrentCulture, "{0} monitors", monitorCount),
};
var lastLaunched = workspace.LastLaunchedTime > 0
? $"Last launched {FormatRelativeTime(workspace.LastLaunchedTime)}"
: "Never launched";
return $"{appsText} • {monitorsText} • {lastLaunched}";
}
private static Details BuildDetails(ProjectWrapper workspace)
{
var appCount = workspace.Applications?.Count ?? 0;
var monitorCount = workspace.MonitorConfiguration?.Count ?? 0;
var lastLaunched = workspace.LastLaunchedTime > 0
? FormatRelativeTime(workspace.LastLaunchedTime)
: "Never launched";
return new Details
{
HeroImage = IconHelpers.FromRelativePath("Assets\\Workspaces.png"),
Title = workspace.Name,
Metadata = BuildAppMetadata(workspace),
};
}
private static IDetailsElement[] BuildAppMetadata(ProjectWrapper workspace)
{
if (workspace.Applications is null || workspace.Applications.Count == 0)
{
return Array.Empty<IDetailsElement>();
}
var elements = new List<IDetailsElement>();
foreach (var app in workspace.Applications)
{
var tags = new List<ITag>();
if (!string.IsNullOrWhiteSpace(app.ApplicationPath))
{
tags.Add(new Tag(app.ApplicationPath));
}
tags.Add(new Tag(string.IsNullOrWhiteSpace(app.Application) ? "App" : app.Application));
if (app.Monitor > 0)
{
tags.Add(new Tag($"Monitor {app.Monitor}"));
}
elements.Add(new DetailsElement
{
Key = string.IsNullOrWhiteSpace(app.Title) ? (app.Application ?? "Application") : app.Title,
Data = new DetailsTags { Tags = tags.ToArray() },
});
}
return elements.ToArray();
}
private static string FormatRelativeTime(long unixSeconds)
{
var lastLaunch = DateTimeOffset.FromUnixTimeSeconds(unixSeconds).UtcDateTime;
var delta = DateTime.UtcNow - lastLaunch;
if (delta.TotalMinutes < 1)
{
return "just now";
}
if (delta.TotalMinutes < 60)
{
return string.Format(CultureInfo.CurrentCulture, "{0} min ago", (int)delta.TotalMinutes);
}
if (delta.TotalHours < 24)
{
return string.Format(CultureInfo.CurrentCulture, "{0} hr ago", (int)delta.TotalHours);
}
return string.Format(CultureInfo.CurrentCulture, "{0} days ago", (int)delta.TotalDays);
}
}

View File

@@ -32,5 +32,5 @@ internal sealed partial class PowerToysListPage : DynamicListPage
RaiseItemsChanged(0);
}
public override IListItem[] GetItems() => ModuleItemsHelper.FilteredItems(SearchText);
public override IListItem[] GetItems() => ModuleCommandCatalog.FilteredItems(SearchText);
}

View File

@@ -2,9 +2,10 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Linq;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
using PowerToysExtension.Helpers;
using PowerToysExtension.Modules;
namespace PowerToysExtension.Pages;
@@ -32,5 +33,5 @@ internal sealed partial class WorkspacesListPage : DynamicListPage
RaiseItemsChanged(0);
}
public override IListItem[] GetItems() => WorkspaceItemsHelper.FilteredItems(SearchText);
public override IListItem[] GetItems() => [.. new WorkspacesModuleCommandProvider().BuildCommands()];
}