mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 17:56:44 +02:00
FancyZones CLI: migrate to System.CommandLine and centralize data I/O (#44344)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This PR refactors FancyZones CLI to use System.CommandLine and consolidates all FancyZones JSON config read/write through FancyZonesEditorCommon data models and FancyZonesDataIO, so CLI and Editor share the same serialization and file paths. For detailed command definitions, see PR #44078 <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #xxx <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
<ItemGroup>
|
||||
<Compile Include="..\editor\FancyZonesEditor\Utils\ParsingResult.cs" Link="ParsingResult.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\CustomLayouts.cs" Link="CustomLayouts.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\FancyZonesPaths.cs" Link="FancyZonesPaths.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\EditorData`1.cs" Link="EditorData`1.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Data\LayoutDefaultSettings.cs" Link="LayoutDefaultSettings.cs" />
|
||||
<Compile Include="..\FancyZonesEditorCommon\Utils\DashCaseNamingPolicy.cs" Link="DashCaseNamingPolicy.cs" />
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
|
||||
using FancyZonesCLI;
|
||||
using FancyZonesCLI.CommandLine;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal abstract class FancyZonesBaseCommand : Command
|
||||
{
|
||||
protected FancyZonesBaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
this.SetHandler(InvokeInternal);
|
||||
}
|
||||
|
||||
protected abstract string Execute(InvocationContext context);
|
||||
|
||||
private void InvokeInternal(InvocationContext context)
|
||||
{
|
||||
Logger.LogInfo($"Executing command '{Name}'");
|
||||
|
||||
if (!FancyZonesCliGuards.IsFancyZonesRunning())
|
||||
{
|
||||
Logger.LogWarning($"Command '{Name}' blocked: FancyZones is not running");
|
||||
context.Console.Error.Write($"Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.{Environment.NewLine}");
|
||||
context.ExitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string output = Execute(context);
|
||||
context.ExitCode = 0;
|
||||
|
||||
Logger.LogInfo($"Command '{Name}' completed successfully");
|
||||
Logger.LogDebug($"Command '{Name}' output length: {output?.Length ?? 0}");
|
||||
|
||||
if (!string.IsNullOrEmpty(output))
|
||||
{
|
||||
context.Console.Out.Write(output);
|
||||
context.Console.Out.Write(Environment.NewLine);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Command '{Name}' failed", ex);
|
||||
context.Console.Error.Write($"Error: {ex.Message}{Environment.NewLine}");
|
||||
context.ExitCode = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
|
||||
using FancyZonesCLI.Utils;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetActiveLayoutCommand()
|
||||
: base("get-active-layout", "Show currently active layout")
|
||||
{
|
||||
AddAlias("active");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// Trigger FancyZones to save current monitor info and read it reliably.
|
||||
var editorParams = EditorParametersRefresh.ReadEditorParametersWithRefresh(
|
||||
() => NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_SAVE_EDITOR_PARAMETERS));
|
||||
|
||||
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Could not get current monitor information.");
|
||||
}
|
||||
|
||||
// Read applied layouts.
|
||||
var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts();
|
||||
|
||||
if (appliedLayouts.AppliedLayouts == null)
|
||||
{
|
||||
return "No layouts configured.";
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
|
||||
|
||||
// Show only layouts for currently connected monitors.
|
||||
for (int i = 0; i < editorParams.Monitors.Count; i++)
|
||||
{
|
||||
var monitor = editorParams.Monitors[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {i + 1}: {monitor.Monitor}");
|
||||
|
||||
var matchedLayout = AppliedLayoutsHelper.FindLayoutForMonitor(
|
||||
appliedLayouts,
|
||||
monitor.Monitor,
|
||||
monitor.MonitorSerialNumber,
|
||||
monitor.MonitorNumber,
|
||||
monitor.VirtualDesktop);
|
||||
|
||||
if (matchedLayout.HasValue)
|
||||
{
|
||||
var layout = matchedLayout.Value.AppliedLayout;
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Layout UUID: {layout.Uuid}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Layout Type: {layout.Type}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {layout.ZoneCount}");
|
||||
|
||||
if (layout.ShowSpacing)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.Spacing}px");
|
||||
}
|
||||
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.SensitivityRadius}px");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" No layout applied");
|
||||
}
|
||||
|
||||
if (i < editorParams.Monitors.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetHotkeysCommand()
|
||||
: base("get-hotkeys", "List all layout hotkeys")
|
||||
{
|
||||
AddAlias("hk");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
var hotkeys = FancyZonesDataIO.ReadLayoutHotkeys();
|
||||
|
||||
if (hotkeys.LayoutHotkeys == null || hotkeys.LayoutHotkeys.Count == 0)
|
||||
{
|
||||
return "No hotkeys configured.";
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("=== Layout Hotkeys ===\n");
|
||||
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
|
||||
|
||||
foreach (var hotkey in hotkeys.LayoutHotkeys.OrderBy(h => h.Key))
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] => {hotkey.LayoutId}");
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetLayoutsCommand()
|
||||
: base("get-layouts", "List available layouts")
|
||||
{
|
||||
AddAlias("ls");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
// Print template layouts.
|
||||
var templatesJson = FancyZonesDataIO.ReadLayoutTemplates();
|
||||
|
||||
if (templatesJson.LayoutTemplates != null)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.LayoutTemplates.Count} total) ===\n");
|
||||
|
||||
for (int i = 0; i < templatesJson.LayoutTemplates.Count; i++)
|
||||
{
|
||||
var template = templatesJson.LayoutTemplates[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"[T{i + 1}] {template.Type}");
|
||||
sb.Append(CultureInfo.InvariantCulture, $" Zones: {template.ZoneCount}");
|
||||
if (template.ShowSpacing && template.Spacing > 0)
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
|
||||
// Draw visual preview.
|
||||
sb.Append(LayoutVisualizer.DrawTemplateLayout(template));
|
||||
|
||||
if (i < templatesJson.LayoutTemplates.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\n");
|
||||
}
|
||||
|
||||
// Print custom layouts.
|
||||
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
|
||||
|
||||
if (customLayouts.CustomLayouts != null)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.CustomLayouts.Count} total) ===");
|
||||
|
||||
for (int i = 0; i < customLayouts.CustomLayouts.Count; i++)
|
||||
{
|
||||
var layout = customLayouts.CustomLayouts[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"[{i + 1}] {layout.Name}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.Uuid}");
|
||||
sb.Append(CultureInfo.InvariantCulture, $" Type: {layout.Type}");
|
||||
|
||||
bool isCanvasLayout = false;
|
||||
if (layout.Info.ValueKind != JsonValueKind.Undefined && layout.Info.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && layout.Info.TryGetProperty("columns", out var cols))
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $" ({rows.GetInt32()}x{cols.GetInt32()} grid)");
|
||||
}
|
||||
else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones))
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $" ({zones.GetArrayLength()} zones)");
|
||||
isCanvasLayout = true;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\n");
|
||||
|
||||
// Draw visual preview.
|
||||
sb.Append(LayoutVisualizer.DrawCustomLayout(layout));
|
||||
|
||||
// Add note for canvas layouts.
|
||||
if (isCanvasLayout)
|
||||
{
|
||||
sb.AppendLine("\n Note: Canvas layout preview is approximate.");
|
||||
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
|
||||
}
|
||||
|
||||
if (i < customLayouts.CustomLayouts.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// 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.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
|
||||
using FancyZonesCLI.Utils;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public GetMonitorsCommand()
|
||||
: base("get-monitors", "List monitors and FancyZones metadata")
|
||||
{
|
||||
AddAlias("m");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// Request FancyZones to save current monitor configuration and read it reliably.
|
||||
EditorParameters.ParamsWrapper editorParams;
|
||||
try
|
||||
{
|
||||
editorParams = EditorParametersRefresh.ReadEditorParametersWithRefresh(
|
||||
() => NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_SAVE_EDITOR_PARAMETERS));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to read monitor information. {ex.Message}{Environment.NewLine}Note: Ensure FancyZones is running to get current monitor information.", ex);
|
||||
}
|
||||
|
||||
if (editorParams.Monitors == null || editorParams.Monitors.Count == 0)
|
||||
{
|
||||
return "No monitors found.";
|
||||
}
|
||||
|
||||
// Also read applied layouts to show which layout is active on each monitor.
|
||||
var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts();
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({editorParams.Monitors.Count} total) ===");
|
||||
sb.AppendLine();
|
||||
|
||||
for (int i = 0; i < editorParams.Monitors.Count; i++)
|
||||
{
|
||||
var monitor = editorParams.Monitors[i];
|
||||
var monitorNum = i + 1;
|
||||
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {monitorNum}:");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor: {monitor.Monitor}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Instance: {monitor.MonitorInstanceId}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Number: {monitor.MonitorNumber}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Serial Number: {monitor.MonitorSerialNumber}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Virtual Desktop: {monitor.VirtualDesktop}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" DPI: {monitor.Dpi}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Resolution: {monitor.MonitorWidth}x{monitor.MonitorHeight}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Work Area: {monitor.WorkAreaWidth}x{monitor.WorkAreaHeight}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Position: ({monitor.LeftCoordinate}, {monitor.TopCoordinate})");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Selected: {monitor.IsSelected}");
|
||||
|
||||
// Find matching applied layout for this monitor using EditorCommon's matching logic.
|
||||
if (appliedLayouts.AppliedLayouts != null)
|
||||
{
|
||||
var matchedLayout = AppliedLayoutsHelper.FindLayoutForMonitor(
|
||||
appliedLayouts,
|
||||
monitor.Monitor,
|
||||
monitor.MonitorSerialNumber,
|
||||
monitor.MonitorNumber,
|
||||
monitor.VirtualDesktop);
|
||||
|
||||
if (matchedLayout != null && matchedLayout.Value.AppliedLayout.Type != null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Active Layout: {matchedLayout.Value.AppliedLayout.Type}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {matchedLayout.Value.AppliedLayout.ZoneCount}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {matchedLayout.Value.AppliedLayout.SensitivityRadius}px");
|
||||
if (!string.IsNullOrEmpty(matchedLayout.Value.AppliedLayout.Uuid) &&
|
||||
matchedLayout.Value.AppliedLayout.Uuid != "{00000000-0000-0000-0000-000000000000}")
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Layout UUID: {matchedLayout.Value.AppliedLayout.Uuid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public OpenEditorCommand()
|
||||
: base("open-editor", "Launch FancyZones layout editor")
|
||||
{
|
||||
AddAlias("e");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
const string FancyZonesEditorToggleEventName = "Local\\FancyZones-ToggleEditorEvent-1e174338-06a3-472b-874d-073b21c62f14";
|
||||
|
||||
// Check if editor is already running
|
||||
var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault();
|
||||
if (existingProcess != null)
|
||||
{
|
||||
NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, FancyZonesEditorToggleEventName);
|
||||
eventHandle.Set();
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to request FancyZones Editor launch. {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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.CommandLine.Invocation;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand
|
||||
{
|
||||
public OpenSettingsCommand()
|
||||
: base("open-settings", "Open FancyZones settings page")
|
||||
{
|
||||
AddAlias("settings");
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// Check in the same directory as the CLI (typical for dev builds)
|
||||
var powertoysExe = Path.Combine(AppContext.BaseDirectory, "PowerToys.exe");
|
||||
if (!File.Exists(powertoysExe))
|
||||
{
|
||||
throw new FileNotFoundException("PowerToys.exe not found. Ensure PowerToys is installed, or run the CLI from the same folder as PowerToys.exe.", powertoysExe);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var process = Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = powertoysExe,
|
||||
Arguments = "--open-settings=FancyZones",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
|
||||
if (process == null)
|
||||
{
|
||||
throw new InvalidOperationException("PowerToys.exe failed to start.");
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to open FancyZones Settings. {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand
|
||||
{
|
||||
private readonly Argument<int> _key;
|
||||
|
||||
public RemoveHotkeyCommand()
|
||||
: base("remove-hotkey", "Remove hotkey assignment")
|
||||
{
|
||||
AddAlias("rhk");
|
||||
|
||||
_key = new Argument<int>("key", "Hotkey index (0-9)");
|
||||
AddArgument(_key);
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// FancyZones running guard is handled by FancyZonesBaseCommand.
|
||||
int key = context.ParseResult.GetValueForArgument(_key);
|
||||
|
||||
var hotkeysWrapper = FancyZonesDataIO.ReadLayoutHotkeys();
|
||||
|
||||
if (hotkeysWrapper.LayoutHotkeys == null)
|
||||
{
|
||||
return "No hotkeys configured.";
|
||||
}
|
||||
|
||||
var hotkeysList = hotkeysWrapper.LayoutHotkeys;
|
||||
var removed = hotkeysList.RemoveAll(h => h.Key == key);
|
||||
if (removed == 0)
|
||||
{
|
||||
return $"No hotkey assigned to key {key}";
|
||||
}
|
||||
|
||||
// Save.
|
||||
var newWrapper = new LayoutHotkeys.LayoutHotkeysWrapper { LayoutHotkeys = hotkeysList };
|
||||
FancyZonesDataIO.WriteLayoutHotkeys(newWrapper);
|
||||
|
||||
// Notify FancyZones.
|
||||
NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Linq;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand
|
||||
{
|
||||
private readonly Argument<int> _key;
|
||||
private readonly Argument<string> _layout;
|
||||
|
||||
public SetHotkeyCommand()
|
||||
: base("set-hotkey", "Assign hotkey (0-9) to a custom layout")
|
||||
{
|
||||
AddAlias("shk");
|
||||
|
||||
_key = new Argument<int>("key", "Hotkey index (0-9)");
|
||||
_layout = new Argument<string>("layout", "Custom layout UUID");
|
||||
|
||||
AddArgument(_key);
|
||||
AddArgument(_layout);
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// FancyZones running guard is handled by FancyZonesBaseCommand.
|
||||
int key = context.ParseResult.GetValueForArgument(_key);
|
||||
string layout = context.ParseResult.GetValueForArgument(_layout);
|
||||
|
||||
if (key < 0 || key > 9)
|
||||
{
|
||||
throw new InvalidOperationException("Key must be between 0 and 9.");
|
||||
}
|
||||
|
||||
// Editor only allows assigning hotkeys to existing custom layouts.
|
||||
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
|
||||
|
||||
CustomLayouts.CustomLayoutWrapper? matchedLayout = null;
|
||||
if (customLayouts.CustomLayouts != null)
|
||||
{
|
||||
foreach (var candidate in customLayouts.CustomLayouts)
|
||||
{
|
||||
if (candidate.Uuid.Equals(layout, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matchedLayout = candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchedLayout.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException($"Layout '{layout}' is not a custom layout UUID.");
|
||||
}
|
||||
|
||||
string layoutName = matchedLayout.Value.Name;
|
||||
|
||||
var hotkeysWrapper = FancyZonesDataIO.ReadLayoutHotkeys();
|
||||
|
||||
var hotkeysList = hotkeysWrapper.LayoutHotkeys ?? new List<LayoutHotkeys.LayoutHotkeyWrapper>();
|
||||
|
||||
// Match editor behavior:
|
||||
// - One key maps to one layout
|
||||
// - One layout maps to at most one key
|
||||
hotkeysList.RemoveAll(h => h.Key == key);
|
||||
hotkeysList.RemoveAll(h => string.Equals(h.LayoutId, layout, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Add new hotkey.
|
||||
hotkeysList.Add(new LayoutHotkeys.LayoutHotkeyWrapper { Key = key, LayoutId = layout });
|
||||
|
||||
// Save.
|
||||
var newWrapper = new LayoutHotkeys.LayoutHotkeysWrapper { LayoutHotkeys = hotkeysList };
|
||||
FancyZonesDataIO.WriteLayoutHotkeys(newWrapper);
|
||||
|
||||
// Notify FancyZones.
|
||||
NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE);
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
// 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.CommandLine;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.Globalization;
|
||||
|
||||
using FancyZonesCLI.Utils;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand
|
||||
{
|
||||
private static readonly string[] AliasesMonitor = ["--monitor", "-m"];
|
||||
private static readonly string[] AliasesAll = ["--all", "-a"];
|
||||
|
||||
private const string DefaultLayoutUuid = "{00000000-0000-0000-0000-000000000000}";
|
||||
|
||||
private readonly Argument<string> _layoutId;
|
||||
private readonly Option<int?> _monitor;
|
||||
private readonly Option<bool> _all;
|
||||
|
||||
public SetLayoutCommand()
|
||||
: base("set-layout", "Set layout by UUID or template name")
|
||||
{
|
||||
AddAlias("s");
|
||||
|
||||
_layoutId = new Argument<string>("layout", "Layout UUID or template type (e.g. focus, columns)");
|
||||
AddArgument(_layoutId);
|
||||
|
||||
_monitor = new Option<int?>(AliasesMonitor, "Apply to monitor N (1-based)");
|
||||
_monitor.AddValidator(result =>
|
||||
{
|
||||
if (result.Tokens.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int? monitor = result.GetValueOrDefault<int?>();
|
||||
if (monitor.HasValue && monitor.Value < 1)
|
||||
{
|
||||
result.ErrorMessage = "Monitor index must be >= 1.";
|
||||
}
|
||||
});
|
||||
|
||||
_all = new Option<bool>(AliasesAll, "Apply to all monitors");
|
||||
|
||||
AddOption(_monitor);
|
||||
AddOption(_all);
|
||||
|
||||
AddValidator(commandResult =>
|
||||
{
|
||||
int? monitor = commandResult.GetValueForOption(_monitor);
|
||||
bool all = commandResult.GetValueForOption(_all);
|
||||
|
||||
if (monitor.HasValue && all)
|
||||
{
|
||||
commandResult.ErrorMessage = "Cannot specify both --monitor and --all.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected override string Execute(InvocationContext context)
|
||||
{
|
||||
// FancyZones running guard is handled by FancyZonesBaseCommand.
|
||||
string layout = context.ParseResult.GetValueForArgument(_layoutId);
|
||||
int? monitor = context.ParseResult.GetValueForOption(_monitor);
|
||||
bool all = context.ParseResult.GetValueForOption(_all);
|
||||
Logger.LogInfo($"SetLayout called with layout: '{layout}', monitor: {(monitor.HasValue ? monitor.Value.ToString(CultureInfo.InvariantCulture) : "<default>")}, all: {all}");
|
||||
|
||||
var (targetCustomLayout, targetTemplate) = ResolveTargetLayout(layout);
|
||||
|
||||
var editorParams = ReadEditorParametersWithRefresh();
|
||||
var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts();
|
||||
appliedLayouts.AppliedLayouts ??= new List<AppliedLayouts.AppliedLayoutWrapper>();
|
||||
|
||||
List<int> monitorsToUpdate = GetMonitorsToUpdate(editorParams, monitor, all);
|
||||
List<AppliedLayouts.AppliedLayoutWrapper> newLayouts = BuildNewLayouts(editorParams, monitorsToUpdate, targetCustomLayout, targetTemplate);
|
||||
var updatedLayouts = MergeWithHistoricalLayouts(appliedLayouts, newLayouts);
|
||||
|
||||
Logger.LogInfo($"Writing {updatedLayouts.AppliedLayouts?.Count ?? 0} layouts to file");
|
||||
FancyZonesDataIO.WriteAppliedLayouts(updatedLayouts);
|
||||
Logger.LogInfo($"Applied layouts file updated for {monitorsToUpdate.Count} monitor(s)");
|
||||
|
||||
NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE);
|
||||
Logger.LogInfo("FancyZones notified of layout change");
|
||||
|
||||
return BuildSuccessMessage(layout, monitor, all);
|
||||
}
|
||||
|
||||
private static string BuildSuccessMessage(string layout, int? monitor, bool all)
|
||||
{
|
||||
if (all)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to all monitors.", layout);
|
||||
}
|
||||
|
||||
if (monitor.HasValue)
|
||||
{
|
||||
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor {1}.", layout, monitor.Value);
|
||||
}
|
||||
|
||||
return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor 1.", layout);
|
||||
}
|
||||
|
||||
private static (CustomLayouts.CustomLayoutWrapper? TargetCustomLayout, LayoutTemplates.TemplateLayoutWrapper? TargetTemplate) ResolveTargetLayout(string layout)
|
||||
{
|
||||
var customLayouts = FancyZonesDataIO.ReadCustomLayouts();
|
||||
CustomLayouts.CustomLayoutWrapper? targetCustomLayout = FindCustomLayout(customLayouts, layout);
|
||||
|
||||
LayoutTemplates.TemplateLayoutWrapper? targetTemplate = null;
|
||||
if (!targetCustomLayout.HasValue || string.IsNullOrEmpty(targetCustomLayout.Value.Uuid))
|
||||
{
|
||||
var templates = FancyZonesDataIO.ReadLayoutTemplates();
|
||||
targetTemplate = FindTemplate(templates, layout);
|
||||
|
||||
if (targetCustomLayout.HasValue && string.IsNullOrEmpty(targetCustomLayout.Value.Uuid))
|
||||
{
|
||||
targetCustomLayout = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetCustomLayout.HasValue && !targetTemplate.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Layout '{layout}' not found{Environment.NewLine}" +
|
||||
"Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')" +
|
||||
$"{Environment.NewLine} For custom layouts, use the UUID from 'get-layouts'");
|
||||
}
|
||||
|
||||
return (targetCustomLayout, targetTemplate);
|
||||
}
|
||||
|
||||
private static CustomLayouts.CustomLayoutWrapper? FindCustomLayout(CustomLayouts.CustomLayoutListWrapper customLayouts, string layout)
|
||||
{
|
||||
if (customLayouts.CustomLayouts == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var customLayout in customLayouts.CustomLayouts)
|
||||
{
|
||||
if (customLayout.Uuid.Equals(layout, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return customLayout;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static LayoutTemplates.TemplateLayoutWrapper? FindTemplate(LayoutTemplates.TemplateLayoutsListWrapper templates, string layout)
|
||||
{
|
||||
if (templates.LayoutTemplates == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var template in templates.LayoutTemplates)
|
||||
{
|
||||
if (template.Type.Equals(layout, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh()
|
||||
{
|
||||
return EditorParametersRefresh.ReadEditorParametersWithRefresh(
|
||||
() => NativeMethods.NotifyFancyZones(NativeMethods.WM_PRIV_SAVE_EDITOR_PARAMETERS));
|
||||
}
|
||||
|
||||
private static List<int> GetMonitorsToUpdate(EditorParameters.ParamsWrapper editorParams, int? monitor, bool all)
|
||||
{
|
||||
var result = new List<int>();
|
||||
|
||||
if (all)
|
||||
{
|
||||
for (int i = 0; i < editorParams.Monitors.Count; i++)
|
||||
{
|
||||
result.Add(i);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
if (monitor.HasValue)
|
||||
{
|
||||
int monitorIndex = monitor.Value - 1; // Convert to 0-based.
|
||||
if (monitorIndex < 0 || monitorIndex >= editorParams.Monitors.Count)
|
||||
{
|
||||
throw new InvalidOperationException($"Monitor {monitor.Value} not found. Available monitors: 1-{editorParams.Monitors.Count}");
|
||||
}
|
||||
|
||||
result.Add(monitorIndex);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Default: first monitor.
|
||||
result.Add(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<AppliedLayouts.AppliedLayoutWrapper> BuildNewLayouts(
|
||||
EditorParameters.ParamsWrapper editorParams,
|
||||
List<int> monitorsToUpdate,
|
||||
CustomLayouts.CustomLayoutWrapper? targetCustomLayout,
|
||||
LayoutTemplates.TemplateLayoutWrapper? targetTemplate)
|
||||
{
|
||||
var newLayouts = new List<AppliedLayouts.AppliedLayoutWrapper>();
|
||||
|
||||
foreach (int monitorIndex in monitorsToUpdate)
|
||||
{
|
||||
var currentMonitor = editorParams.Monitors[monitorIndex];
|
||||
|
||||
var (layoutUuid, layoutType, showSpacing, spacing, zoneCount, sensitivityRadius) =
|
||||
GetLayoutSettings(targetCustomLayout, targetTemplate);
|
||||
|
||||
var deviceId = new AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper
|
||||
{
|
||||
Monitor = currentMonitor.Monitor,
|
||||
MonitorInstance = currentMonitor.MonitorInstanceId,
|
||||
MonitorNumber = currentMonitor.MonitorNumber,
|
||||
SerialNumber = currentMonitor.MonitorSerialNumber,
|
||||
VirtualDesktop = currentMonitor.VirtualDesktop,
|
||||
};
|
||||
|
||||
newLayouts.Add(new AppliedLayouts.AppliedLayoutWrapper
|
||||
{
|
||||
Device = deviceId,
|
||||
AppliedLayout = new AppliedLayouts.AppliedLayoutWrapper.LayoutWrapper
|
||||
{
|
||||
Uuid = layoutUuid,
|
||||
Type = layoutType,
|
||||
ShowSpacing = showSpacing,
|
||||
Spacing = spacing,
|
||||
ZoneCount = zoneCount,
|
||||
SensitivityRadius = sensitivityRadius,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (newLayouts.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Internal error - no monitors to update.");
|
||||
}
|
||||
|
||||
return newLayouts;
|
||||
}
|
||||
|
||||
private static (string LayoutUuid, string LayoutType, bool ShowSpacing, int Spacing, int ZoneCount, int SensitivityRadius) GetLayoutSettings(
|
||||
CustomLayouts.CustomLayoutWrapper? targetCustomLayout,
|
||||
LayoutTemplates.TemplateLayoutWrapper? targetTemplate)
|
||||
{
|
||||
if (targetCustomLayout.HasValue)
|
||||
{
|
||||
var customLayoutsSerializer = new CustomLayouts();
|
||||
string type = targetCustomLayout.Value.Type?.ToLowerInvariant() ?? string.Empty;
|
||||
|
||||
bool showSpacing = false;
|
||||
int spacing = 0;
|
||||
int zoneCount = 0;
|
||||
int sensitivityRadius = 20;
|
||||
|
||||
if (type == "canvas")
|
||||
{
|
||||
var info = customLayoutsSerializer.CanvasFromJsonElement(targetCustomLayout.Value.Info.GetRawText());
|
||||
zoneCount = info.Zones?.Count ?? 0;
|
||||
sensitivityRadius = info.SensitivityRadius;
|
||||
}
|
||||
else if (type == "grid")
|
||||
{
|
||||
var info = customLayoutsSerializer.GridFromJsonElement(targetCustomLayout.Value.Info.GetRawText());
|
||||
showSpacing = info.ShowSpacing;
|
||||
spacing = info.Spacing;
|
||||
sensitivityRadius = info.SensitivityRadius;
|
||||
|
||||
if (info.CellChildMap != null)
|
||||
{
|
||||
var uniqueZoneIds = new HashSet<int>();
|
||||
|
||||
for (int r = 0; r < info.CellChildMap.Length; r++)
|
||||
{
|
||||
int[] row = info.CellChildMap[r];
|
||||
if (row == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int c = 0; c < row.Length; c++)
|
||||
{
|
||||
uniqueZoneIds.Add(row[c]);
|
||||
}
|
||||
}
|
||||
|
||||
zoneCount = uniqueZoneIds.Count;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unsupported custom layout type '{targetCustomLayout.Value.Type}'.");
|
||||
}
|
||||
|
||||
return (
|
||||
targetCustomLayout.Value.Uuid,
|
||||
Constants.CustomLayoutJsonTag,
|
||||
ShowSpacing: showSpacing,
|
||||
Spacing: spacing,
|
||||
ZoneCount: zoneCount,
|
||||
SensitivityRadius: sensitivityRadius);
|
||||
}
|
||||
|
||||
if (targetTemplate.HasValue)
|
||||
{
|
||||
return (
|
||||
DefaultLayoutUuid,
|
||||
targetTemplate.Value.Type,
|
||||
targetTemplate.Value.ShowSpacing,
|
||||
targetTemplate.Value.Spacing,
|
||||
targetTemplate.Value.ZoneCount,
|
||||
targetTemplate.Value.SensitivityRadius);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Internal error - no layout selected.");
|
||||
}
|
||||
|
||||
private static AppliedLayouts.AppliedLayoutsListWrapper MergeWithHistoricalLayouts(
|
||||
AppliedLayouts.AppliedLayoutsListWrapper existingLayouts,
|
||||
List<AppliedLayouts.AppliedLayoutWrapper> newLayouts)
|
||||
{
|
||||
var mergedLayoutsList = new List<AppliedLayouts.AppliedLayoutWrapper>();
|
||||
mergedLayoutsList.AddRange(newLayouts);
|
||||
|
||||
if (existingLayouts.AppliedLayouts != null)
|
||||
{
|
||||
foreach (var existingLayout in existingLayouts.AppliedLayouts)
|
||||
{
|
||||
bool isUpdated = false;
|
||||
|
||||
foreach (var newLayout in newLayouts)
|
||||
{
|
||||
if (AppliedLayoutsHelper.MatchesDevice(
|
||||
existingLayout.Device,
|
||||
newLayout.Device.Monitor,
|
||||
newLayout.Device.SerialNumber,
|
||||
newLayout.Device.MonitorNumber,
|
||||
newLayout.Device.VirtualDesktop))
|
||||
{
|
||||
isUpdated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isUpdated)
|
||||
{
|
||||
mergedLayoutsList.Add(existingLayout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new AppliedLayouts.AppliedLayoutsListWrapper
|
||||
{
|
||||
AppliedLayouts = mergedLayoutsList,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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 System.CommandLine;
|
||||
using FancyZonesCLI.CommandLine.Commands;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine;
|
||||
|
||||
internal static class FancyZonesCliCommandFactory
|
||||
{
|
||||
public static RootCommand CreateRootCommand()
|
||||
{
|
||||
var root = new RootCommand("FancyZones CLI - Command line interface for FancyZones");
|
||||
|
||||
root.AddCommand(new OpenEditorCommand());
|
||||
root.AddCommand(new GetMonitorsCommand());
|
||||
root.AddCommand(new GetLayoutsCommand());
|
||||
root.AddCommand(new GetActiveLayoutCommand());
|
||||
root.AddCommand(new SetLayoutCommand());
|
||||
root.AddCommand(new OpenSettingsCommand());
|
||||
root.AddCommand(new GetHotkeysCommand());
|
||||
root.AddCommand(new SetHotkeyCommand());
|
||||
root.AddCommand(new RemoveHotkeyCommand());
|
||||
|
||||
return root;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine;
|
||||
|
||||
internal static class FancyZonesCliGuards
|
||||
{
|
||||
public static bool IsFancyZonesRunning()
|
||||
{
|
||||
try
|
||||
{
|
||||
return Process.GetProcessesByName("PowerToys.FancyZones").Length != 0;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
|
||||
namespace FancyZonesCLI.CommandLine;
|
||||
|
||||
internal static class FancyZonesCliUsage
|
||||
{
|
||||
public static void PrintUsage()
|
||||
{
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine("FancyZones CLI - Command line interface for FancyZones");
|
||||
Console.WriteLine();
|
||||
|
||||
var cmd = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
|
||||
Console.WriteLine("Usage: FancyZonesCLI [command] [options]");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("Options:");
|
||||
foreach (var option in cmd.Options)
|
||||
{
|
||||
var aliases = string.Join(", ", option.Aliases);
|
||||
var description = option.Description ?? string.Empty;
|
||||
Console.WriteLine($" {aliases,-30} {description}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Commands:");
|
||||
foreach (var command in cmd.Subcommands)
|
||||
{
|
||||
if (command.IsHidden)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Format: "command-name <args>, alias"
|
||||
string argsLabel = string.Join(" ", command.Arguments.Select(a => $"<{a.Name}>"));
|
||||
string baseLabel = string.IsNullOrEmpty(argsLabel) ? command.Name : $"{command.Name} {argsLabel}";
|
||||
|
||||
// Find first alias (Aliases includes Name)
|
||||
string alias = command.Aliases.FirstOrDefault(a => !string.Equals(a, command.Name, StringComparison.OrdinalIgnoreCase));
|
||||
string label = string.IsNullOrEmpty(alias) ? baseLabel : $"{baseLabel}, {alias}";
|
||||
|
||||
var description = command.Description ?? string.Empty;
|
||||
Console.WriteLine($" {label,-30} {description}");
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Examples:");
|
||||
Console.WriteLine(" FancyZonesCLI --help");
|
||||
Console.WriteLine(" FancyZonesCLI --version");
|
||||
Console.WriteLine(" FancyZonesCLI get-monitors");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout focus");
|
||||
Console.WriteLine(" FancyZonesCLI set-layout <uuid> --monitor 1");
|
||||
Console.WriteLine(" FancyZonesCLI get-hotkeys");
|
||||
}
|
||||
}
|
||||
@@ -1,90 +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.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace FancyZonesCLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Editor and Settings commands.
|
||||
/// </summary>
|
||||
internal static class EditorCommands
|
||||
{
|
||||
public static (int ExitCode, string Output) OpenEditor()
|
||||
{
|
||||
var editorExe = "PowerToys.FancyZonesEditor.exe";
|
||||
|
||||
// Check if editor-parameters.json exists
|
||||
if (!FancyZonesData.EditorParametersExist())
|
||||
{
|
||||
return (1, "Error: editor-parameters.json not found.\nPlease launch FancyZones Editor using Win+` (Win+Backtick) hotkey first.");
|
||||
}
|
||||
|
||||
// Check if editor is already running
|
||||
var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault();
|
||||
if (existingProcess != null)
|
||||
{
|
||||
NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle);
|
||||
return (0, "FancyZones Editor is already running. Brought window to foreground.");
|
||||
}
|
||||
|
||||
// Only check same directory as CLI
|
||||
var editorPath = Path.Combine(AppContext.BaseDirectory, editorExe);
|
||||
|
||||
if (File.Exists(editorPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = editorPath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
return (0, "FancyZones Editor launched successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (1, $"Failed to launch: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return (1, $"Error: Could not find {editorExe} in {AppContext.BaseDirectory}");
|
||||
}
|
||||
|
||||
public static (int ExitCode, string Output) OpenSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Find PowerToys.exe in common locations
|
||||
string powertoysExe = null;
|
||||
|
||||
// Check in the same directory as the CLI (typical for dev builds)
|
||||
var sameDirPath = Path.Combine(AppContext.BaseDirectory, "PowerToys.exe");
|
||||
if (File.Exists(sameDirPath))
|
||||
{
|
||||
powertoysExe = sameDirPath;
|
||||
}
|
||||
|
||||
if (powertoysExe == null)
|
||||
{
|
||||
return (1, "Error: PowerToys.exe not found. Please ensure PowerToys is installed.");
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = powertoysExe,
|
||||
Arguments = "--open-settings=FancyZones",
|
||||
UseShellExecute = false,
|
||||
});
|
||||
return (0, "FancyZones Settings opened successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (1, $"Error: Failed to open FancyZones Settings. {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +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.Linq;
|
||||
|
||||
namespace FancyZonesCLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Hotkey-related commands.
|
||||
/// </summary>
|
||||
internal static class HotkeyCommands
|
||||
{
|
||||
public static (int ExitCode, string Output) GetHotkeys()
|
||||
{
|
||||
var hotkeys = FancyZonesData.ReadLayoutHotkeys();
|
||||
if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0)
|
||||
{
|
||||
return (0, "No hotkeys configured.");
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("=== Layout Hotkeys ===\n");
|
||||
sb.AppendLine("Press Win + Ctrl + Alt + <number> to switch layouts:\n");
|
||||
|
||||
foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key))
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] => {hotkey.LayoutId}");
|
||||
}
|
||||
|
||||
return (0, sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
public static (int ExitCode, string Output) SetHotkey(int key, string layoutUuid, Action<uint> notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate)
|
||||
{
|
||||
if (key < 0 || key > 9)
|
||||
{
|
||||
return (1, "Error: Key must be between 0 and 9");
|
||||
}
|
||||
|
||||
// Check if this is a custom layout UUID
|
||||
var customLayouts = FancyZonesData.ReadCustomLayouts();
|
||||
var matchedLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(layoutUuid, StringComparison.OrdinalIgnoreCase));
|
||||
bool isCustomLayout = matchedLayout != null;
|
||||
string layoutName = matchedLayout?.Name ?? layoutUuid;
|
||||
|
||||
var hotkeys = FancyZonesData.ReadLayoutHotkeys() ?? new LayoutHotkeys();
|
||||
|
||||
hotkeys.Hotkeys ??= new List<LayoutHotkey>();
|
||||
|
||||
// Remove existing hotkey for this key
|
||||
hotkeys.Hotkeys.RemoveAll(h => h.Key == key);
|
||||
|
||||
// Add new hotkey
|
||||
hotkeys.Hotkeys.Add(new LayoutHotkey { Key = key, LayoutId = layoutUuid });
|
||||
|
||||
// Save
|
||||
FancyZonesData.WriteLayoutHotkeys(hotkeys);
|
||||
|
||||
// Notify FancyZones
|
||||
notifyFancyZones(wmPrivLayoutHotkeysFileUpdate);
|
||||
|
||||
if (isCustomLayout)
|
||||
{
|
||||
return (0, $"✓ Hotkey {key} assigned to custom layout '{layoutName}'\n Press Win + Ctrl + Alt + {key} to switch to this layout");
|
||||
}
|
||||
else
|
||||
{
|
||||
return (0, $"⚠ Warning: Hotkey {key} assigned to '{layoutUuid}'\n Note: FancyZones hotkeys only work with CUSTOM layouts.\n Template layouts (focus, columns, rows, etc.) cannot be used with hotkeys.\n Create a custom layout in the FancyZones Editor to use this hotkey.");
|
||||
}
|
||||
}
|
||||
|
||||
public static (int ExitCode, string Output) RemoveHotkey(int key, Action<uint> notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate)
|
||||
{
|
||||
var hotkeys = FancyZonesData.ReadLayoutHotkeys();
|
||||
if (hotkeys?.Hotkeys == null)
|
||||
{
|
||||
return (0, $"No hotkey assigned to key {key}");
|
||||
}
|
||||
|
||||
var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key);
|
||||
if (removed == 0)
|
||||
{
|
||||
return (0, $"No hotkey assigned to key {key}");
|
||||
}
|
||||
|
||||
// Save
|
||||
FancyZonesData.WriteLayoutHotkeys(hotkeys);
|
||||
|
||||
// Notify FancyZones
|
||||
notifyFancyZones(wmPrivLayoutHotkeysFileUpdate);
|
||||
|
||||
return (0, $"Hotkey {key} removed");
|
||||
}
|
||||
}
|
||||
@@ -1,276 +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.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace FancyZonesCLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Layout-related commands.
|
||||
/// </summary>
|
||||
internal static class LayoutCommands
|
||||
{
|
||||
public static (int ExitCode, string Output) GetLayouts()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
// Print template layouts
|
||||
var templatesJson = FancyZonesData.ReadLayoutTemplates();
|
||||
if (templatesJson?.Templates != null)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n");
|
||||
|
||||
for (int i = 0; i < templatesJson.Templates.Count; i++)
|
||||
{
|
||||
var template = templatesJson.Templates[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"[T{i + 1}] {template.Type}");
|
||||
sb.Append(CultureInfo.InvariantCulture, $" Zones: {template.ZoneCount}");
|
||||
if (template.ShowSpacing && template.Spacing > 0)
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
|
||||
// Draw visual preview
|
||||
sb.Append(LayoutVisualizer.DrawTemplateLayout(template));
|
||||
|
||||
if (i < templatesJson.Templates.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\n");
|
||||
}
|
||||
|
||||
// Print custom layouts
|
||||
var customLayouts = FancyZonesData.ReadCustomLayouts();
|
||||
if (customLayouts?.Layouts != null)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.Layouts.Count} total) ===");
|
||||
|
||||
for (int i = 0; i < customLayouts.Layouts.Count; i++)
|
||||
{
|
||||
var layout = customLayouts.Layouts[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"[{i + 1}] {layout.Name}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.Uuid}");
|
||||
sb.Append(CultureInfo.InvariantCulture, $" Type: {layout.Type}");
|
||||
|
||||
bool isCanvasLayout = false;
|
||||
if (layout.Info.ValueKind != JsonValueKind.Undefined && layout.Info.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && layout.Info.TryGetProperty("columns", out var cols))
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $" ({rows.GetInt32()}x{cols.GetInt32()} grid)");
|
||||
}
|
||||
else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones))
|
||||
{
|
||||
sb.Append(CultureInfo.InvariantCulture, $" ({zones.GetArrayLength()} zones)");
|
||||
isCanvasLayout = true;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\n");
|
||||
|
||||
// Draw visual preview
|
||||
sb.Append(LayoutVisualizer.DrawCustomLayout(layout));
|
||||
|
||||
// Add note for canvas layouts
|
||||
if (isCanvasLayout)
|
||||
{
|
||||
sb.AppendLine("\n Note: Canvas layout preview is approximate.");
|
||||
sb.AppendLine(" Open FancyZones Editor for precise zone boundaries.");
|
||||
}
|
||||
|
||||
if (i < customLayouts.Layouts.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout.");
|
||||
}
|
||||
|
||||
return (0, sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
public static (int ExitCode, string Output) GetActiveLayout()
|
||||
{
|
||||
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
|
||||
{
|
||||
return (1, $"Error: {error}");
|
||||
}
|
||||
|
||||
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
|
||||
{
|
||||
return (0, "No active layouts found.");
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n");
|
||||
|
||||
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
|
||||
{
|
||||
var layout = appliedLayouts.Layouts[i];
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {i + 1}:");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Name: {layout.AppliedLayout.Type}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.AppliedLayout.Uuid}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Type: {layout.AppliedLayout.Type} ({layout.AppliedLayout.ZoneCount} zones)");
|
||||
|
||||
if (layout.AppliedLayout.ShowSpacing)
|
||||
{
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.AppliedLayout.Spacing}px");
|
||||
}
|
||||
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px");
|
||||
|
||||
if (i < appliedLayouts.Layouts.Count - 1)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return (0, sb.ToString().TrimEnd());
|
||||
}
|
||||
|
||||
public static (int ExitCode, string Output) SetLayout(string[] args, Action<uint> notifyFancyZones, uint wmPrivAppliedLayoutsFileUpdate)
|
||||
{
|
||||
Logger.LogInfo($"SetLayout called with args: [{string.Join(", ", args)}]");
|
||||
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return (1, "Error: set-layout requires a UUID parameter");
|
||||
}
|
||||
|
||||
string uuid = args[0];
|
||||
int? targetMonitor = null;
|
||||
bool applyToAll = false;
|
||||
|
||||
// Parse options
|
||||
for (int i = 1; i < args.Length; i++)
|
||||
{
|
||||
if (args[i] == "--monitor" && i + 1 < args.Length)
|
||||
{
|
||||
if (int.TryParse(args[i + 1], out int monitorNum))
|
||||
{
|
||||
targetMonitor = monitorNum;
|
||||
i++; // Skip next arg
|
||||
}
|
||||
else
|
||||
{
|
||||
return (1, $"Error: Invalid monitor number: {args[i + 1]}");
|
||||
}
|
||||
}
|
||||
else if (args[i] == "--all")
|
||||
{
|
||||
applyToAll = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetMonitor.HasValue && applyToAll)
|
||||
{
|
||||
return (1, "Error: Cannot specify both --monitor and --all");
|
||||
}
|
||||
|
||||
// Try to find layout in custom layouts first (by UUID)
|
||||
var customLayouts = FancyZonesData.ReadCustomLayouts();
|
||||
var targetCustomLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(uuid, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// If not found in custom layouts, try template layouts (by type name)
|
||||
TemplateLayout targetTemplate = null;
|
||||
if (targetCustomLayout == null)
|
||||
{
|
||||
var templates = FancyZonesData.ReadLayoutTemplates();
|
||||
targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (targetCustomLayout == null && targetTemplate == null)
|
||||
{
|
||||
return (1, $"Error: Layout '{uuid}' not found\nTip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')\n For custom layouts, use the UUID from 'get-layouts'");
|
||||
}
|
||||
|
||||
// Read current applied layouts
|
||||
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
|
||||
{
|
||||
return (1, $"Error: {error}");
|
||||
}
|
||||
|
||||
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
|
||||
{
|
||||
return (1, "Error: No monitors configured");
|
||||
}
|
||||
|
||||
// Determine which monitors to update
|
||||
List<int> monitorsToUpdate = new List<int>();
|
||||
if (applyToAll)
|
||||
{
|
||||
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
|
||||
{
|
||||
monitorsToUpdate.Add(i);
|
||||
}
|
||||
}
|
||||
else if (targetMonitor.HasValue)
|
||||
{
|
||||
int monitorIndex = targetMonitor.Value - 1; // Convert to 0-based
|
||||
if (monitorIndex < 0 || monitorIndex >= appliedLayouts.Layouts.Count)
|
||||
{
|
||||
return (1, $"Error: Monitor {targetMonitor.Value} not found. Available monitors: 1-{appliedLayouts.Layouts.Count}");
|
||||
}
|
||||
|
||||
monitorsToUpdate.Add(monitorIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default: first monitor
|
||||
monitorsToUpdate.Add(0);
|
||||
}
|
||||
|
||||
// Update selected monitors
|
||||
foreach (int monitorIndex in monitorsToUpdate)
|
||||
{
|
||||
if (targetCustomLayout != null)
|
||||
{
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Uuid = targetCustomLayout.Uuid;
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Type = targetCustomLayout.Type;
|
||||
}
|
||||
else if (targetTemplate != null)
|
||||
{
|
||||
// For templates, use all-zeros UUID and the template type
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Uuid = "{00000000-0000-0000-0000-000000000000}";
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Type = targetTemplate.Type;
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.ZoneCount = targetTemplate.ZoneCount;
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.ShowSpacing = targetTemplate.ShowSpacing;
|
||||
appliedLayouts.Layouts[monitorIndex].AppliedLayout.Spacing = targetTemplate.Spacing;
|
||||
}
|
||||
}
|
||||
|
||||
// Write back to file
|
||||
FancyZonesData.WriteAppliedLayouts(appliedLayouts);
|
||||
Logger.LogInfo($"Applied layouts file updated for {monitorsToUpdate.Count} monitor(s)");
|
||||
|
||||
// Notify FancyZones to reload
|
||||
notifyFancyZones(wmPrivAppliedLayoutsFileUpdate);
|
||||
Logger.LogInfo("FancyZones notified of layout change");
|
||||
|
||||
string layoutName = targetCustomLayout?.Name ?? targetTemplate?.Type ?? uuid;
|
||||
if (applyToAll)
|
||||
{
|
||||
return (0, $"Layout '{layoutName}' applied to all {monitorsToUpdate.Count} monitors");
|
||||
}
|
||||
else if (targetMonitor.HasValue)
|
||||
{
|
||||
return (0, $"Layout '{layoutName}' applied to monitor {targetMonitor.Value}");
|
||||
}
|
||||
else
|
||||
{
|
||||
return (0, $"Layout '{layoutName}' applied to monitor 1");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +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.Globalization;
|
||||
|
||||
namespace FancyZonesCLI.Commands;
|
||||
|
||||
/// <summary>
|
||||
/// Monitor-related commands.
|
||||
/// </summary>
|
||||
internal static class MonitorCommands
|
||||
{
|
||||
public static (int ExitCode, string Output) GetMonitors()
|
||||
{
|
||||
if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error))
|
||||
{
|
||||
return (1, $"Error: {error}");
|
||||
}
|
||||
|
||||
if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0)
|
||||
{
|
||||
return (0, "No monitors found.");
|
||||
}
|
||||
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({appliedLayouts.Layouts.Count} total) ===");
|
||||
sb.AppendLine();
|
||||
|
||||
for (int i = 0; i < appliedLayouts.Layouts.Count; i++)
|
||||
{
|
||||
var layout = appliedLayouts.Layouts[i];
|
||||
var monitorNum = i + 1;
|
||||
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {monitorNum}:");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor: {layout.Device.Monitor}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Instance: {layout.Device.MonitorInstance}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Number: {layout.Device.MonitorNumber}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Serial Number: {layout.Device.SerialNumber}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Virtual Desktop: {layout.Device.VirtualDesktop}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Active Layout: {layout.AppliedLayout.Type}");
|
||||
sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {layout.AppliedLayout.ZoneCount}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return (0, sb.ToString().TrimEnd());
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.SelfContained.props" />
|
||||
<Import Project="..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.FancyZonesCLI</AssemblyTitle>
|
||||
@@ -10,8 +9,6 @@
|
||||
<Description>PowerToys FancyZones CLI</Description>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Platforms>x64;ARM64</Platforms>
|
||||
<PublishAot>true</PublishAot>
|
||||
<DisableRuntimeMarshalling>true</DisableRuntimeMarshalling>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)</OutputPath>
|
||||
@@ -21,9 +18,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Text.Json" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWin32" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\FancyZonesEditorCommon\FancyZonesEditorCommon.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App" />
|
||||
|
||||
@@ -1,142 +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.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace FancyZonesCLI;
|
||||
|
||||
/// <summary>
|
||||
/// Provides methods to read and write FancyZones configuration data.
|
||||
/// </summary>
|
||||
internal static class FancyZonesData
|
||||
{
|
||||
/// <summary>
|
||||
/// Try to read applied layouts configuration.
|
||||
/// </summary>
|
||||
public static bool TryReadAppliedLayouts(out AppliedLayouts result, out string error)
|
||||
{
|
||||
return TryReadJsonFile(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts, out result, out error);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read applied layouts or return null if not found.
|
||||
/// </summary>
|
||||
public static AppliedLayouts ReadAppliedLayouts()
|
||||
{
|
||||
return ReadJsonFileOrDefault(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write applied layouts configuration.
|
||||
/// </summary>
|
||||
public static void WriteAppliedLayouts(AppliedLayouts layouts)
|
||||
{
|
||||
WriteJsonFile(FancyZonesPaths.AppliedLayouts, layouts, FancyZonesJsonContext.Default.AppliedLayouts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read custom layouts or return null if not found.
|
||||
/// </summary>
|
||||
public static CustomLayouts ReadCustomLayouts()
|
||||
{
|
||||
return ReadJsonFileOrDefault(FancyZonesPaths.CustomLayouts, FancyZonesJsonContext.Default.CustomLayouts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read layout templates or return null if not found.
|
||||
/// </summary>
|
||||
public static LayoutTemplates ReadLayoutTemplates()
|
||||
{
|
||||
return ReadJsonFileOrDefault(FancyZonesPaths.LayoutTemplates, FancyZonesJsonContext.Default.LayoutTemplates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read layout hotkeys or return null if not found.
|
||||
/// </summary>
|
||||
public static LayoutHotkeys ReadLayoutHotkeys()
|
||||
{
|
||||
return ReadJsonFileOrDefault(FancyZonesPaths.LayoutHotkeys, FancyZonesJsonContext.Default.LayoutHotkeys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write layout hotkeys configuration.
|
||||
/// </summary>
|
||||
public static void WriteLayoutHotkeys(LayoutHotkeys hotkeys)
|
||||
{
|
||||
WriteJsonFile(FancyZonesPaths.LayoutHotkeys, hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if editor parameters file exists.
|
||||
/// </summary>
|
||||
public static bool EditorParametersExist()
|
||||
{
|
||||
return File.Exists(FancyZonesPaths.EditorParameters);
|
||||
}
|
||||
|
||||
private static bool TryReadJsonFile<T>(string filePath, JsonTypeInfo<T> jsonTypeInfo, out T result, out string error)
|
||||
where T : class
|
||||
{
|
||||
result = null;
|
||||
error = null;
|
||||
|
||||
Logger.LogDebug($"Reading file: {filePath}");
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
error = $"File not found: {Path.GetFileName(filePath)}";
|
||||
Logger.LogWarning(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
result = JsonSerializer.Deserialize(json, jsonTypeInfo);
|
||||
if (result == null)
|
||||
{
|
||||
error = $"Failed to parse {Path.GetFileName(filePath)}";
|
||||
Logger.LogError(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.LogDebug($"Successfully read {Path.GetFileName(filePath)}");
|
||||
return true;
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
error = $"JSON parse error in {Path.GetFileName(filePath)}: {ex.Message}";
|
||||
Logger.LogError(error, ex);
|
||||
return false;
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
error = $"Failed to read {Path.GetFileName(filePath)}: {ex.Message}";
|
||||
Logger.LogError(error, ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static T ReadJsonFileOrDefault<T>(string filePath, JsonTypeInfo<T> jsonTypeInfo, T defaultValue = null)
|
||||
where T : class
|
||||
{
|
||||
if (TryReadJsonFile(filePath, jsonTypeInfo, out var result, out _))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static void WriteJsonFile<T>(string filePath, T data, JsonTypeInfo<T> jsonTypeInfo)
|
||||
{
|
||||
Logger.LogDebug($"Writing file: {filePath}");
|
||||
var json = JsonSerializer.Serialize(data, jsonTypeInfo);
|
||||
File.WriteAllText(filePath, json);
|
||||
Logger.LogInfo($"Successfully wrote {Path.GetFileName(filePath)}");
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,13 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
|
||||
namespace FancyZonesCLI;
|
||||
|
||||
public static class LayoutVisualizer
|
||||
{
|
||||
public static string DrawTemplateLayout(TemplateLayout template)
|
||||
public static string DrawTemplateLayout(LayoutTemplates.TemplateLayoutWrapper template)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(" Visual Preview:");
|
||||
@@ -62,7 +63,7 @@ public static class LayoutVisualizer
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static string DrawCustomLayout(CustomLayout layout)
|
||||
public static string DrawCustomLayout(CustomLayouts.CustomLayoutWrapper layout)
|
||||
{
|
||||
if (layout.Info.ValueKind == JsonValueKind.Undefined || layout.Info.ValueKind == JsonValueKind.Null)
|
||||
{
|
||||
@@ -426,6 +427,11 @@ public static class LayoutVisualizer
|
||||
const int displayWidth = 49;
|
||||
const int displayHeight = 15;
|
||||
|
||||
if (refWidth <= 0 || refHeight <= 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Create a 2D array to track which zones occupy each position
|
||||
var zoneGrid = new List<int>[displayHeight, displayWidth];
|
||||
for (int i = 0; i < displayHeight; i++)
|
||||
@@ -442,10 +448,13 @@ public static class LayoutVisualizer
|
||||
|
||||
foreach (var zone in zones.EnumerateArray())
|
||||
{
|
||||
int x = zone.GetProperty("X").GetInt32();
|
||||
int y = zone.GetProperty("Y").GetInt32();
|
||||
int w = zone.GetProperty("width").GetInt32();
|
||||
int h = zone.GetProperty("height").GetInt32();
|
||||
if (!TryGetInt32Property(zone, "x", "X", out int x) ||
|
||||
!TryGetInt32Property(zone, "y", "Y", out int y) ||
|
||||
!TryGetInt32Property(zone, "width", "Width", out int w) ||
|
||||
!TryGetInt32Property(zone, "height", "Height", out int h))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int dx = Math.Max(0, Math.Min(displayWidth - 1, x * displayWidth / refWidth));
|
||||
int dy = Math.Max(0, Math.Min(displayHeight - 1, y * displayHeight / refHeight));
|
||||
@@ -547,4 +556,23 @@ public static class LayoutVisualizer
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static bool TryGetInt32Property(JsonElement element, string primaryName, string fallbackName, out int value)
|
||||
{
|
||||
if (element.TryGetProperty(primaryName, out var property) || element.TryGetProperty(fallbackName, out property))
|
||||
{
|
||||
if (property.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
return property.TryGetInt32(out value);
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return int.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,137 +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.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace FancyZonesCLI;
|
||||
|
||||
// JSON Source Generator for AOT compatibility
|
||||
[JsonSerializable(typeof(LayoutTemplates))]
|
||||
[JsonSerializable(typeof(CustomLayouts))]
|
||||
[JsonSerializable(typeof(AppliedLayouts))]
|
||||
[JsonSerializable(typeof(LayoutHotkeys))]
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
internal partial class FancyZonesJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
// Layout Templates
|
||||
public sealed class LayoutTemplates
|
||||
{
|
||||
[JsonPropertyName("layout-templates")]
|
||||
public List<TemplateLayout> Templates { get; set; }
|
||||
}
|
||||
|
||||
public sealed class TemplateLayout
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("zone-count")]
|
||||
public int ZoneCount { get; set; }
|
||||
|
||||
[JsonPropertyName("show-spacing")]
|
||||
public bool ShowSpacing { get; set; }
|
||||
|
||||
[JsonPropertyName("spacing")]
|
||||
public int Spacing { get; set; }
|
||||
|
||||
[JsonPropertyName("sensitivity-radius")]
|
||||
public int SensitivityRadius { get; set; }
|
||||
}
|
||||
|
||||
// Custom Layouts
|
||||
public sealed class CustomLayouts
|
||||
{
|
||||
[JsonPropertyName("custom-layouts")]
|
||||
public List<CustomLayout> Layouts { get; set; }
|
||||
}
|
||||
|
||||
public sealed class CustomLayout
|
||||
{
|
||||
[JsonPropertyName("uuid")]
|
||||
public string Uuid { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("info")]
|
||||
public JsonElement Info { get; set; }
|
||||
}
|
||||
|
||||
// Applied Layouts
|
||||
public sealed class AppliedLayouts
|
||||
{
|
||||
[JsonPropertyName("applied-layouts")]
|
||||
public List<AppliedLayoutWrapper> Layouts { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AppliedLayoutWrapper
|
||||
{
|
||||
[JsonPropertyName("device")]
|
||||
public DeviceInfo Device { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("applied-layout")]
|
||||
public AppliedLayoutInfo AppliedLayout { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class DeviceInfo
|
||||
{
|
||||
[JsonPropertyName("monitor")]
|
||||
public string Monitor { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("monitor-instance")]
|
||||
public string MonitorInstance { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("monitor-number")]
|
||||
public int MonitorNumber { get; set; }
|
||||
|
||||
[JsonPropertyName("serial-number")]
|
||||
public string SerialNumber { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("virtual-desktop")]
|
||||
public string VirtualDesktop { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class AppliedLayoutInfo
|
||||
{
|
||||
[JsonPropertyName("uuid")]
|
||||
public string Uuid { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("show-spacing")]
|
||||
public bool ShowSpacing { get; set; }
|
||||
|
||||
[JsonPropertyName("spacing")]
|
||||
public int Spacing { get; set; }
|
||||
|
||||
[JsonPropertyName("zone-count")]
|
||||
public int ZoneCount { get; set; }
|
||||
|
||||
[JsonPropertyName("sensitivity-radius")]
|
||||
public int SensitivityRadius { get; set; }
|
||||
}
|
||||
|
||||
// Layout Hotkeys
|
||||
public sealed class LayoutHotkeys
|
||||
{
|
||||
[JsonPropertyName("layout-hotkeys")]
|
||||
public List<LayoutHotkey> Hotkeys { get; set; }
|
||||
}
|
||||
|
||||
public sealed class LayoutHotkey
|
||||
{
|
||||
[JsonPropertyName("key")]
|
||||
public int Key { get; set; }
|
||||
|
||||
[JsonPropertyName("layout-id")]
|
||||
public string LayoutId { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ internal static class NativeMethods
|
||||
// Registered Windows messages for notifying FancyZones
|
||||
private static uint wmPrivAppliedLayoutsFileUpdate;
|
||||
private static uint wmPrivLayoutHotkeysFileUpdate;
|
||||
private static uint wmPrivSaveEditorParameters;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Windows message ID for applied layouts file update notification.
|
||||
@@ -26,6 +27,11 @@ internal static class NativeMethods
|
||||
/// </summary>
|
||||
public static uint WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE => wmPrivLayoutHotkeysFileUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Windows message ID used to request saving editor-parameters.json.
|
||||
/// </summary>
|
||||
public static uint WM_PRIV_SAVE_EDITOR_PARAMETERS => wmPrivSaveEditorParameters;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the Windows messages used for FancyZones notifications.
|
||||
/// </summary>
|
||||
@@ -33,6 +39,7 @@ internal static class NativeMethods
|
||||
{
|
||||
wmPrivAppliedLayoutsFileUpdate = PInvoke.RegisterWindowMessage("{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}");
|
||||
wmPrivLayoutHotkeysFileUpdate = PInvoke.RegisterWindowMessage("{07229b7e-4f22-4357-b136-33c289be2295}");
|
||||
wmPrivSaveEditorParameters = PInvoke.RegisterWindowMessage("{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -3,113 +3,44 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.CommandLine;
|
||||
using System.Linq;
|
||||
using FancyZonesCLI.Commands;
|
||||
using System.Threading.Tasks;
|
||||
using FancyZonesCLI.CommandLine;
|
||||
|
||||
namespace FancyZonesCLI;
|
||||
|
||||
internal sealed class Program
|
||||
{
|
||||
private static int Main(string[] args)
|
||||
private static async Task<int> Main(string[] args)
|
||||
{
|
||||
// Initialize logger
|
||||
Logger.InitializeLogger();
|
||||
Logger.LogInfo($"CLI invoked with args: [{string.Join(", ", args)}]");
|
||||
|
||||
// Initialize Windows messages
|
||||
// Initialize Windows messages used to notify FancyZones.
|
||||
NativeMethods.InitializeWindowMessages();
|
||||
|
||||
(int ExitCode, string Output) result;
|
||||
|
||||
if (args.Length == 0)
|
||||
// Intercept help requests early and print custom usage.
|
||||
if (args.Any(a => string.Equals(a, "--help", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-h", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(a, "-?", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
result = (1, GetUsageText());
|
||||
FancyZonesCliUsage.PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
RootCommand rootCommand = FancyZonesCliCommandFactory.CreateRootCommand();
|
||||
int exitCode = await rootCommand.InvokeAsync(args);
|
||||
|
||||
if (exitCode == 0)
|
||||
{
|
||||
Logger.LogInfo("Command completed successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
var command = args[0].ToLowerInvariant();
|
||||
|
||||
result = command switch
|
||||
{
|
||||
"open-editor" or "editor" or "e" => EditorCommands.OpenEditor(),
|
||||
"get-monitors" or "monitors" or "m" => MonitorCommands.GetMonitors(),
|
||||
"get-layouts" or "layouts" or "ls" => LayoutCommands.GetLayouts(),
|
||||
"get-active-layout" or "active" or "get-active" or "a" => LayoutCommands.GetActiveLayout(),
|
||||
"set-layout" or "set" or "s" => args.Length >= 2
|
||||
? LayoutCommands.SetLayout(args.Skip(1).ToArray(), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE)
|
||||
: (1, "Error: set-layout requires a UUID parameter"),
|
||||
"open-settings" or "settings" => EditorCommands.OpenSettings(),
|
||||
"get-hotkeys" or "hotkeys" or "hk" => HotkeyCommands.GetHotkeys(),
|
||||
"set-hotkey" or "shk" => args.Length >= 3
|
||||
? HotkeyCommands.SetHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), args[2], NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE)
|
||||
: (1, "Error: set-hotkey requires <key> <uuid>"),
|
||||
"remove-hotkey" or "rhk" => args.Length >= 2
|
||||
? HotkeyCommands.RemoveHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE)
|
||||
: (1, "Error: remove-hotkey requires <key>"),
|
||||
"help" or "--help" or "-h" => (0, GetUsageText()),
|
||||
_ => (1, $"Error: Unknown command: {command}\n\n{GetUsageText()}"),
|
||||
};
|
||||
Logger.LogWarning($"Command failed with exit code {exitCode}");
|
||||
}
|
||||
|
||||
// Log result
|
||||
if (result.ExitCode == 0)
|
||||
{
|
||||
Logger.LogInfo($"Command completed successfully");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"Command failed with exit code {result.ExitCode}: {result.Output}");
|
||||
}
|
||||
|
||||
// Output result
|
||||
if (!string.IsNullOrEmpty(result.Output))
|
||||
{
|
||||
Console.WriteLine(result.Output);
|
||||
}
|
||||
|
||||
return result.ExitCode;
|
||||
}
|
||||
|
||||
private static string GetUsageText()
|
||||
{
|
||||
return """
|
||||
FancyZones CLI - Command line interface for FancyZones
|
||||
======================================================
|
||||
|
||||
Usage: FancyZonesCLI.exe <command> [options]
|
||||
|
||||
Commands:
|
||||
open-editor (editor, e) Launch FancyZones layout editor
|
||||
get-monitors (monitors, m) List all monitors and their properties
|
||||
get-layouts (layouts, ls) List all available layouts
|
||||
get-active-layout (get-active, active, a)
|
||||
Show currently active layout
|
||||
set-layout (set, s) <uuid> [options]
|
||||
Set layout by UUID
|
||||
--monitor <n> Apply to monitor N (1-based)
|
||||
--all Apply to all monitors
|
||||
open-settings (settings) Open FancyZones settings page
|
||||
get-hotkeys (hotkeys, hk) List all layout hotkeys
|
||||
set-hotkey (shk) <key> <uuid> Assign hotkey (0-9) to CUSTOM layout
|
||||
Note: Only custom layouts work with hotkeys
|
||||
remove-hotkey (rhk) <key> Remove hotkey assignment
|
||||
help Show this help message
|
||||
|
||||
|
||||
Examples:
|
||||
FancyZonesCLI.exe e # Open editor (short)
|
||||
FancyZonesCLI.exe m # List monitors (short)
|
||||
FancyZonesCLI.exe ls # List layouts (short)
|
||||
FancyZonesCLI.exe a # Get active layout (short)
|
||||
FancyZonesCLI.exe s focus --all # Set layout (short)
|
||||
FancyZonesCLI.exe open-editor # Open editor (long)
|
||||
FancyZonesCLI.exe get-monitors
|
||||
FancyZonesCLI.exe get-layouts
|
||||
FancyZonesCLI.exe set-layout {12345678-1234-1234-1234-123456789012}
|
||||
FancyZonesCLI.exe set-layout focus --monitor 2
|
||||
FancyZonesCLI.exe set-layout columns --all
|
||||
FancyZonesCLI.exe set-hotkey 3 {12345678-1234-1234-1234-123456789012}
|
||||
""";
|
||||
return exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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 FancyZonesEditorCommon.Data;
|
||||
|
||||
namespace FancyZonesCLI.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for managing applied layouts across monitors.
|
||||
/// CLI-only business logic for matching, finding, and updating applied layouts.
|
||||
/// </summary>
|
||||
internal static class AppliedLayoutsHelper
|
||||
{
|
||||
public const string DefaultVirtualDesktopGuid = "{00000000-0000-0000-0000-000000000000}";
|
||||
|
||||
public static bool MatchesDevice(
|
||||
AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper device,
|
||||
string monitorName,
|
||||
string serialNumber,
|
||||
int monitorNumber,
|
||||
string virtualDesktop)
|
||||
{
|
||||
// Must match monitor name
|
||||
if (device.Monitor != monitorName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must match virtual desktop
|
||||
if (device.VirtualDesktop != virtualDesktop)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If serial numbers are both available, they must match
|
||||
if (!string.IsNullOrEmpty(device.SerialNumber) && !string.IsNullOrEmpty(serialNumber))
|
||||
{
|
||||
if (device.SerialNumber != serialNumber)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here: Monitor name, VirtualDesktop, and SerialNumber (if available) all match
|
||||
// MonitorInstance and MonitorNumber can vary, so we accept any value
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool MatchesDeviceWithDefaultVirtualDesktop(
|
||||
AppliedLayouts.AppliedLayoutWrapper.DeviceIdWrapper device,
|
||||
string monitorName,
|
||||
string serialNumber,
|
||||
int monitorNumber,
|
||||
string virtualDesktop)
|
||||
{
|
||||
if (device.VirtualDesktop == DefaultVirtualDesktopGuid)
|
||||
{
|
||||
// For this one layout record only, match any virtual desktop.
|
||||
return device.Monitor == monitorName;
|
||||
}
|
||||
|
||||
return MatchesDevice(device, monitorName, serialNumber, monitorNumber, virtualDesktop);
|
||||
}
|
||||
|
||||
public static AppliedLayouts.AppliedLayoutWrapper? FindLayoutForMonitor(
|
||||
AppliedLayouts.AppliedLayoutsListWrapper layouts,
|
||||
string monitorName,
|
||||
string serialNumber,
|
||||
int monitorNumber,
|
||||
string virtualDesktop)
|
||||
{
|
||||
if (layouts.AppliedLayouts == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var layout in layouts.AppliedLayouts)
|
||||
{
|
||||
if (MatchesDevice(layout.Device, monitorName, serialNumber, monitorNumber, virtualDesktop))
|
||||
{
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
|
||||
using FancyZonesEditorCommon.Data;
|
||||
using FancyZonesEditorCommon.Utils;
|
||||
|
||||
namespace FancyZonesCLI.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper for requesting FancyZones to save editor-parameters.json and reading it reliably.
|
||||
/// </summary>
|
||||
internal static class EditorParametersRefresh
|
||||
{
|
||||
public static EditorParameters.ParamsWrapper ReadEditorParametersWithRefresh(Action requestSave)
|
||||
{
|
||||
const int maxWaitMilliseconds = 500;
|
||||
const int pollIntervalMilliseconds = 50;
|
||||
|
||||
string filePath = FancyZonesPaths.EditorParameters;
|
||||
DateTime lastWriteBefore = File.Exists(filePath) ? File.GetLastWriteTimeUtc(filePath) : DateTime.MinValue;
|
||||
|
||||
requestSave();
|
||||
|
||||
int elapsedMilliseconds = 0;
|
||||
while (elapsedMilliseconds < maxWaitMilliseconds)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
DateTime lastWriteNow = File.GetLastWriteTimeUtc(filePath);
|
||||
|
||||
// Prefer reading after the file is updated, but don't block forever if the
|
||||
// timestamp resolution is coarse or FancyZones rewrites identical content.
|
||||
if (lastWriteNow >= lastWriteBefore || elapsedMilliseconds > 100)
|
||||
{
|
||||
var editorParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
if (editorParams.Monitors != null && editorParams.Monitors.Count > 0)
|
||||
{
|
||||
return editorParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is FileNotFoundException || ex is IOException || ex is UnauthorizedAccessException || ex is JsonException)
|
||||
{
|
||||
// File may be mid-write/locked or temporarily invalid JSON; retry.
|
||||
}
|
||||
|
||||
Thread.Sleep(pollIntervalMilliseconds);
|
||||
elapsedMilliseconds += pollIntervalMilliseconds;
|
||||
}
|
||||
|
||||
var finalParams = FancyZonesDataIO.ReadEditorParameters();
|
||||
if (finalParams.Monitors == null || finalParams.Monitors.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not get current monitor information (timed out after {maxWaitMilliseconds}ms waiting for '{Path.GetFileName(filePath)}').");
|
||||
}
|
||||
|
||||
return finalParams;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\applied-layouts.json";
|
||||
return FancyZonesPaths.AppliedLayouts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\custom-layouts.json";
|
||||
return FancyZonesPaths.CustomLayouts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\default-layouts.json";
|
||||
return FancyZonesPaths.DefaultLayouts;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\editor-parameters.json";
|
||||
return FancyZonesPaths.EditorParameters;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace FancyZonesCLI;
|
||||
namespace FancyZonesEditorCommon.Data;
|
||||
|
||||
/// <summary>
|
||||
/// Provides paths to FancyZones configuration files.
|
||||
/// </summary>
|
||||
internal static class FancyZonesPaths
|
||||
public static class FancyZonesPaths
|
||||
{
|
||||
private static readonly string DataPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
@@ -27,4 +27,6 @@ internal static class FancyZonesPaths
|
||||
public static string LayoutHotkeys => Path.Combine(DataPath, "layout-hotkeys.json");
|
||||
|
||||
public static string EditorParameters => Path.Combine(DataPath, "editor-parameters.json");
|
||||
|
||||
public static string DefaultLayouts => Path.Combine(DataPath, "default-layouts.json");
|
||||
}
|
||||
@@ -14,7 +14,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\layout-hotkeys.json";
|
||||
return FancyZonesPaths.LayoutHotkeys;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace FancyZonesEditorCommon.Data
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetDataFolder() + "\\Microsoft\\PowerToys\\FancyZones\\layout-templates.json";
|
||||
return FancyZonesPaths.LayoutTemplates;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using FancyZonesEditorCommon.Data;
|
||||
|
||||
namespace FancyZonesEditorCommon.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Unified helper for all FancyZones data file I/O operations.
|
||||
/// Centralizes reading and writing of all JSON configuration files.
|
||||
/// </summary>
|
||||
public static class FancyZonesDataIO
|
||||
{
|
||||
private static TWrapper ReadData<TData, TWrapper>(
|
||||
Func<TData> createInstance,
|
||||
Func<TData, string> fileSelector,
|
||||
Func<TData, string, TWrapper> readFunc)
|
||||
{
|
||||
var instance = createInstance();
|
||||
string filePath = fileSelector(instance);
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
throw new FileNotFoundException($"File not found: {Path.GetFileName(filePath)}", filePath);
|
||||
}
|
||||
|
||||
return readFunc(instance, filePath);
|
||||
}
|
||||
|
||||
private static void WriteData<TData, TWrapper>(
|
||||
Func<TData> createInstance,
|
||||
Func<TData, string> fileSelector,
|
||||
Func<TData, TWrapper, string> serializeFunc,
|
||||
TWrapper data)
|
||||
{
|
||||
var instance = createInstance();
|
||||
var filePath = fileSelector(instance);
|
||||
|
||||
IOUtils ioUtils = new IOUtils();
|
||||
ioUtils.WriteFile(filePath, serializeFunc(instance, data));
|
||||
}
|
||||
|
||||
// AppliedLayouts operations
|
||||
public static AppliedLayouts.AppliedLayoutsListWrapper ReadAppliedLayouts()
|
||||
{
|
||||
return ReadData(
|
||||
() => new AppliedLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteAppliedLayouts(AppliedLayouts.AppliedLayoutsListWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new AppliedLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
|
||||
// CustomLayouts operations
|
||||
public static CustomLayouts.CustomLayoutListWrapper ReadCustomLayouts()
|
||||
{
|
||||
return ReadData(
|
||||
() => new CustomLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteCustomLayouts(CustomLayouts.CustomLayoutListWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new CustomLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
|
||||
// LayoutTemplates operations
|
||||
public static LayoutTemplates.TemplateLayoutsListWrapper ReadLayoutTemplates()
|
||||
{
|
||||
return ReadData(
|
||||
() => new LayoutTemplates(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteLayoutTemplates(LayoutTemplates.TemplateLayoutsListWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new LayoutTemplates(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
|
||||
// LayoutHotkeys operations
|
||||
public static LayoutHotkeys.LayoutHotkeysWrapper ReadLayoutHotkeys()
|
||||
{
|
||||
return ReadData(
|
||||
() => new LayoutHotkeys(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteLayoutHotkeys(LayoutHotkeys.LayoutHotkeysWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new LayoutHotkeys(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
|
||||
// EditorParameters operations
|
||||
public static EditorParameters.ParamsWrapper ReadEditorParameters()
|
||||
{
|
||||
return ReadData(
|
||||
() => new EditorParameters(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteEditorParameters(EditorParameters.ParamsWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new EditorParameters(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
|
||||
// DefaultLayouts operations
|
||||
public static DefaultLayouts.DefaultLayoutsListWrapper ReadDefaultLayouts()
|
||||
{
|
||||
return ReadData(
|
||||
() => new DefaultLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, file) => instance.Read(file));
|
||||
}
|
||||
|
||||
public static void WriteDefaultLayouts(DefaultLayouts.DefaultLayoutsListWrapper data)
|
||||
{
|
||||
WriteData(
|
||||
() => new DefaultLayouts(),
|
||||
instance => instance.File,
|
||||
(instance, wrapper) => instance.Serialize(wrapper),
|
||||
data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -728,6 +728,13 @@ LRESULT FancyZones::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lpa
|
||||
{
|
||||
FancyZonesSettings::instance().LoadSettings();
|
||||
}
|
||||
else if (message == WM_PRIV_SAVE_EDITOR_PARAMETERS)
|
||||
{
|
||||
if (!EditorParameters::Save(m_workAreaConfiguration, m_dpiUnawareThread))
|
||||
{
|
||||
Logger::warn(L"Failed to save editor-parameters.json");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
|
||||
@@ -18,6 +18,7 @@ UINT WM_PRIV_DEFAULT_LAYOUTS_FILE_UPDATE;
|
||||
UINT WM_PRIV_SNAP_HOTKEY;
|
||||
UINT WM_PRIV_QUICK_LAYOUT_KEY;
|
||||
UINT WM_PRIV_SETTINGS_CHANGED;
|
||||
UINT WM_PRIV_SAVE_EDITOR_PARAMETERS;
|
||||
|
||||
std::once_flag init_flag;
|
||||
|
||||
@@ -40,5 +41,6 @@ void InitializeWinhookEventIds()
|
||||
WM_PRIV_SNAP_HOTKEY = RegisterWindowMessage(L"{72f4fd8e-23f1-43ab-bbbc-029363df9a84}");
|
||||
WM_PRIV_QUICK_LAYOUT_KEY = RegisterWindowMessage(L"{15baab3d-c67b-4a15-aFF0-13610e05e947}");
|
||||
WM_PRIV_SETTINGS_CHANGED = RegisterWindowMessage(L"{89ca3Daa-bf2d-4e73-9f3f-c60716364e27}");
|
||||
WM_PRIV_SAVE_EDITOR_PARAMETERS = RegisterWindowMessage(L"{d8f9c0e3-5d77-4e83-8a4f-7c704c2bfb4a}");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ extern UINT WM_PRIV_DEFAULT_LAYOUTS_FILE_UPDATE; // Scheduled when the watched d
|
||||
extern UINT WM_PRIV_SNAP_HOTKEY; // Scheduled when we receive a snap hotkey key down press
|
||||
extern UINT WM_PRIV_QUICK_LAYOUT_KEY; // Scheduled when we receive a key down press to quickly apply a layout
|
||||
extern UINT WM_PRIV_SETTINGS_CHANGED; // Scheduled when a watched settings file is updated
|
||||
extern UINT WM_PRIV_SAVE_EDITOR_PARAMETERS; // Scheduled to request saving editor-parameters.json
|
||||
|
||||
void InitializeWinhookEventIds();
|
||||
|
||||
Reference in New Issue
Block a user