From 153c6c851c3e95d79b4873e45623a0cdea467806 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 4 Dec 2025 20:06:20 +0800 Subject: [PATCH] add fancyzones cli --- .pipelines/ESRPSigning_core.json | 2 + PowerToys.sln | 11 + .../FancyZonesCLI/FancyZonesCLI.csproj | 26 + .../FancyZonesCLI/LayoutVisualizer.cs | 625 ++++++++++++++ .../fancyzones/FancyZonesCLI/Models.cs | 137 ++++ .../fancyzones/FancyZonesCLI/Program.cs | 764 ++++++++++++++++++ 6 files changed, 1565 insertions(+) create mode 100644 src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj create mode 100644 src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Models.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Program.cs diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json index 83289fa102..f4e3e1ba38 100644 --- a/.pipelines/ESRPSigning_core.json +++ b/.pipelines/ESRPSigning_core.json @@ -60,6 +60,8 @@ "PowerToys.FancyZonesEditorCommon.dll", "PowerToys.FancyZonesModuleInterface.dll", "PowerToys.FancyZones.exe", + "FancyZonesCLI.exe", + "FancyZonesCLI.dll", "PowerToys.GcodePreviewHandler.dll", "PowerToys.GcodePreviewHandler.exe", diff --git a/PowerToys.sln b/PowerToys.sln index 4e42cc9e30..9f499133f4 100644 --- a/PowerToys.sln +++ b/PowerToys.sln @@ -55,6 +55,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PackageIdentity", "src\Pack EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesEditor", "src\modules\fancyzones\editor\FancyZonesEditor\FancyZonesEditor.csproj", "{5CCC8468-DEC8-4D36-99D4-5C891BEBD481}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FancyZonesCLI", "src\modules\fancyzones\FancyZonesCLI\FancyZonesCLI.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "powerrename", "powerrename", "{89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRenameExt", "src\modules\powerrename\dll\PowerRenameExt.vcxproj", "{B25AC7A5-FB9F-4789-B392-D5C85E948670}" @@ -894,6 +896,14 @@ Global {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|ARM64.Build.0 = Release|ARM64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.ActiveCfg = Release|x64 {5CCC8468-DEC8-4D36-99D4-5C891BEBD481}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|ARM64.Build.0 = Debug|ARM64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.ActiveCfg = Release|ARM64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|ARM64.Build.0 = Release|ARM64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|ARM64.ActiveCfg = Debug|ARM64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|ARM64.Build.0 = Debug|ARM64 {B25AC7A5-FB9F-4789-B392-D5C85E948670}.Debug|x64.ActiveCfg = Debug|x64 @@ -3067,6 +3077,7 @@ Global {9C6A7905-72D4-4BF5-B256-ABFDAEF68AE9} = {264B412F-DB8B-4CF8-A74B-96998B183045} {1A066C63-64B3-45F8-92FE-664E1CCE8077} = {1AFB6476-670D-4E80-A464-657E01DFF482} {5CCC8468-DEC8-4D36-99D4-5C891BEBD481} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {D1D6BC88-09AE-4FB4-AD24-5DED46A791DD} {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} = {4574FDD0-F61D-4376-98BF-E5A1262C11EC} {B25AC7A5-FB9F-4789-B392-D5C85E948670} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} {51920F1F-C28C-4ADF-8660-4238766796C2} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3} diff --git a/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj b/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj new file mode 100644 index 0000000000..a543100a2a --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj @@ -0,0 +1,26 @@ + + + + + + + + PowerToys.FancyZonesCLI + PowerToys FancyZones Command Line Interface + PowerToys FancyZones CLI + + Exe + x64;ARM64 + true + false + false + ..\..\..\..\$(Platform)\$(Configuration) + FancyZonesCLI + $(NoWarn);SA1500;SA1402;CA1852 + + + + + + + diff --git a/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs b/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs new file mode 100644 index 0000000000..c86a0f7fe1 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs @@ -0,0 +1,625 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; + +namespace FancyZonesCLI; + +public static class LayoutVisualizer +{ + public static void DrawTemplateLayout(TemplateLayout template) + { + Console.WriteLine(" Visual Preview:"); + + switch (template.Type.ToLowerInvariant()) + { + case "focus": + DrawFocusLayout(template.ZoneCount > 0 ? template.ZoneCount : 3); + break; + case "columns": + DrawGridLayout(1, template.ZoneCount > 0 ? template.ZoneCount : 3); + break; + case "rows": + DrawGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3, 1); + break; + case "grid": + // Grid layout: calculate rows and columns from zone count + // Algorithm from GridLayoutModel.InitGrid() - tries to make it close to square + // with cols >= rows preference + int zoneCount = template.ZoneCount > 0 ? template.ZoneCount : 3; + int rows = 1; + while (zoneCount / rows >= rows) + { + rows++; + } + + rows--; + int cols = zoneCount / rows; + if (zoneCount % rows != 0) + { + cols++; + } + + DrawGridLayoutWithZoneCount(rows, cols, zoneCount); + break; + case "priority-grid": + DrawPriorityGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3); + break; + case "blank": + Console.WriteLine(" (No zones)"); + break; + default: + Console.WriteLine($" ({template.Type} layout)"); + break; + } + } + + public static void DrawCustomLayout(CustomLayout layout) + { + if (layout.Info.ValueKind == JsonValueKind.Undefined || layout.Info.ValueKind == JsonValueKind.Null) + { + return; + } + + Console.WriteLine(" Visual Preview:"); + + if (layout.Type == "grid" && + layout.Info.TryGetProperty("rows", out var rows) && + layout.Info.TryGetProperty("columns", out var cols)) + { + int r = rows.GetInt32(); + int c = cols.GetInt32(); + + // Check if there's a cell-child-map (merged cells) + if (layout.Info.TryGetProperty("cell-child-map", out var cellMap)) + { + DrawGridLayoutWithMergedCells(r, c, cellMap); + } + else + { + int height = r >= 4 ? 12 : 8; + DrawGridLayout(r, c, 30, height); + } + } + else if (layout.Type == "canvas" && + layout.Info.TryGetProperty("zones", out var zones) && + layout.Info.TryGetProperty("ref-width", out var refWidth) && + layout.Info.TryGetProperty("ref-height", out var refHeight)) + { + DrawCanvasLayout(zones, refWidth.GetInt32(), refHeight.GetInt32()); + } + } + + private static void DrawFocusLayout(int zoneCount = 3) + { + // Focus layout: overlapping zones with cascading offset + // Show first 2, ellipsis, and last 1 if more than 4 zones + if (zoneCount == 1) + { + Console.WriteLine(" +-------+"); + Console.WriteLine(" | |"); + Console.WriteLine(" | |"); + Console.WriteLine(" +-------+"); + } + else if (zoneCount == 2) + { + Console.WriteLine(" +-------+"); + Console.WriteLine(" | |"); + Console.WriteLine(" | +-------+"); + Console.WriteLine(" +-| |"); + Console.WriteLine(" | |"); + Console.WriteLine(" +-------+"); + } + else + { + Console.WriteLine(" +-------+"); + Console.WriteLine(" | |"); + Console.WriteLine(" | +-------+"); + Console.WriteLine(" +-| |"); + Console.WriteLine(" | +-------+"); + Console.WriteLine(" +-| |"); + + // Middle ellipsis + Console.WriteLine(" ..."); + Console.WriteLine($" (total: {zoneCount} zones)"); + Console.WriteLine(" ..."); + + // Show indication of last zone (without full indent) + Console.WriteLine(" | +-------+"); + Console.WriteLine(" +-| |"); + Console.WriteLine(" | |"); + Console.WriteLine(" +-------+"); + } + } + + private static void DrawPriorityGridLayout(int zoneCount = 3) + { + // Priority Grid has predefined layouts for zone counts 1-11 + // Data format from GridLayoutModel._priorityData + if (zoneCount >= 1 && zoneCount <= 11) + { + int[,] cellMap = GetPriorityGridCellMap(zoneCount); + DrawGridLayoutWithCellMap(cellMap); + } + else + { + // > 11 zones: fallback to grid layout + int rows = 1; + while (zoneCount / rows >= rows) + { + rows++; + } + + rows--; + int cols = zoneCount / rows; + if (zoneCount % rows != 0) + { + cols++; + } + + DrawGridLayoutWithZoneCount(rows, cols, zoneCount); + } + } + + private static int[,] GetPriorityGridCellMap(int zoneCount) + { + // Parsed from Editor's _priorityData byte arrays + return zoneCount switch + { + 1 => new int[,] { { 0 } }, + 2 => new int[,] { { 0, 1 } }, + 3 => new int[,] { { 0, 1, 2 } }, + 4 => new int[,] { { 0, 1, 2 }, { 0, 1, 3 } }, + 5 => new int[,] { { 0, 1, 2 }, { 3, 1, 4 } }, + 6 => new int[,] { { 0, 1, 2 }, { 0, 1, 3 }, { 4, 1, 5 } }, + 7 => new int[,] { { 0, 1, 2 }, { 3, 1, 4 }, { 5, 1, 6 } }, + 8 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 2, 7 } }, + 9 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 2, 5 }, { 6, 1, 7, 8 } }, + 10 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 1, 8, 9 } }, + 11 => new int[,] { { 0, 1, 2, 3 }, { 4, 1, 5, 6 }, { 7, 8, 9, 10 } }, + _ => new int[,] { { 0 } }, + }; + } + + private static void DrawGridLayoutWithCellMap(int[,] cellMap, int width = 30, int height = 8) + { + int rows = cellMap.GetLength(0); + int cols = cellMap.GetLength(1); + + int cellWidth = width / cols; + int cellHeight = height / rows; + + for (int r = 0; r < rows; r++) + { + // Top border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + // Check if this cell should merge with the cell above + bool mergeTop = r > 0 && cellMap[r, c] == cellMap[r - 1, c]; + + // Check if this cell should merge with the cell to the left + bool mergeLeft = c > 0 && cellMap[r, c] == cellMap[r, c - 1]; + + if (mergeTop) + { + if (mergeLeft) + { + Console.Write(new string(' ', cellWidth)); + } + else + { + Console.Write(new string(' ', cellWidth - 1)); + Console.Write("+"); + } + } + else + { + if (mergeLeft) + { + Console.Write(new string('-', cellWidth)); + } + else + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + } + } + + Console.WriteLine(); + + // Cell content + for (int h = 0; h < cellHeight - 1; h++) + { + Console.Write(" "); + for (int c = 0; c < cols; c++) + { + bool mergeLeft = c > 0 && cellMap[r, c] == cellMap[r, c - 1]; + if (mergeLeft) + { + Console.Write(" "); + } + else + { + Console.Write("|"); + } + + Console.Write(new string(' ', cellWidth - 1)); + } + + Console.WriteLine("|"); + } + } + + // Bottom border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + + Console.WriteLine(); + } + + private static void DrawGridLayoutWithMergedCells(int rows, int cols, JsonElement cellMap) + { + const int displayWidth = 39; + const int displayHeight = 12; + + // Build zone map from cell-child-map + int[,] zoneMap = new int[rows, cols]; + for (int r = 0; r < rows; r++) + { + var rowArray = cellMap[r]; + for (int c = 0; c < cols; c++) + { + zoneMap[r, c] = rowArray[c].GetInt32(); + } + } + + // Find unique zones and their count + var zones = new HashSet(); + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + zones.Add(zoneMap[r, c]); + } + } + + int cellHeight = displayHeight / rows; + int cellWidth = displayWidth / cols; + + // Draw top border + Console.Write(" +"); + Console.Write(new string('-', displayWidth)); + Console.WriteLine("+"); + + // Draw rows + for (int r = 0; r < rows; r++) + { + // For each row, find the column range of each zone + var zoneRanges = new Dictionary(); + for (int c = 0; c < cols; c++) + { + int zone = zoneMap[r, c]; + if (zoneRanges.TryGetValue(zone, out var range)) + { + zoneRanges[zone] = (range.Start, c); + } + else + { + zoneRanges[zone] = (c, c); + } + } + + for (int h = 0; h < cellHeight; h++) + { + Console.Write(" |"); + + for (int c = 0; c < cols; c++) + { + int currentZone = zoneMap[r, c]; + int leftZone = c > 0 ? zoneMap[r, c - 1] : -1; + bool needLeftBorder = c > 0 && currentZone != leftZone; + + // Check if this zone has a top border + bool zoneHasTopBorder = false; + if (r > 0 && h == 0) + { + int topZone = zoneMap[r - 1, c]; + zoneHasTopBorder = currentZone != topZone; + } + + // Draw left border if needed + if (needLeftBorder) + { + Console.Write("|"); + + // Fill rest of cell + for (int w = 1; w < cellWidth; w++) + { + Console.Write(zoneHasTopBorder ? "-" : " "); + } + } + else + { + // No left border, fill entire cell + for (int w = 0; w < cellWidth; w++) + { + Console.Write(zoneHasTopBorder ? "-" : " "); + } + } + } + + Console.WriteLine("|"); + } + } + + // Draw bottom border + Console.Write(" +"); + Console.Write(new string('-', displayWidth)); + Console.WriteLine("+"); + } + + public static void DrawGridLayoutWithZoneCount(int rows, int cols, int zoneCount, int width = 30, int height = 8) + { + // Build zone map like Editor's InitGrid + int[,] zoneMap = new int[rows, cols]; + int index = 0; + for (int r = 0; r < rows; r++) + { + for (int c = 0; c < cols; c++) + { + zoneMap[r, c] = index++; + if (index == zoneCount) + { + index--; // Remaining cells use the last zone index + } + } + } + + int cellWidth = width / cols; + int cellHeight = height / rows; + + for (int r = 0; r < rows; r++) + { + // Top border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + // Check if this cell should merge with the previous one (same zone) + bool mergeLeft = c > 0 && zoneMap[r, c] == zoneMap[r, c - 1]; + if (mergeLeft) + { + Console.Write(new string('-', cellWidth)); + } + else + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + } + + Console.WriteLine(); + + // Cell content + for (int h = 0; h < cellHeight - 1; h++) + { + Console.Write(" "); + for (int c = 0; c < cols; c++) + { + // Check if this cell should merge with the previous one + bool mergeLeft = c > 0 && zoneMap[r, c] == zoneMap[r, c - 1]; + if (mergeLeft) + { + Console.Write(" "); + } + else + { + Console.Write("|"); + } + + Console.Write(new string(' ', cellWidth - 1)); + } + + Console.WriteLine("|"); + } + } + + // Bottom border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + + Console.WriteLine(); + } + + public static void DrawGridLayout(int rows, int cols, int width = 30, int height = 8) + { + int cellWidth = width / cols; + int cellHeight = height / rows; + + for (int r = 0; r < rows; r++) + { + // Top border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + + Console.WriteLine(); + + // Cell content + for (int h = 0; h < cellHeight - 1; h++) + { + Console.Write(" "); + for (int c = 0; c < cols; c++) + { + Console.Write("|"); + Console.Write(new string(' ', cellWidth - 1)); + } + + Console.WriteLine("|"); + } + } + + // Bottom border + Console.Write(" +"); + for (int c = 0; c < cols; c++) + { + Console.Write(new string('-', cellWidth - 1)); + Console.Write("+"); + } + + Console.WriteLine(); + } + + private static void DrawCanvasLayout(JsonElement zones, int refWidth, int refHeight) + { + const int displayWidth = 49; + const int displayHeight = 15; + + // Create a 2D array to track which zones occupy each position + var zoneGrid = new List[displayHeight, displayWidth]; + for (int i = 0; i < displayHeight; i++) + { + for (int j = 0; j < displayWidth; j++) + { + zoneGrid[i, j] = new List(); + } + } + + // Map each zone to the grid + int zoneId = 0; + var zoneList = new List<(int X, int Y, int Width, int Height, int Id)>(); + + 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(); + + int dx = Math.Max(0, Math.Min(displayWidth - 1, x * displayWidth / refWidth)); + int dy = Math.Max(0, Math.Min(displayHeight - 1, y * displayHeight / refHeight)); + int dw = Math.Max(3, w * displayWidth / refWidth); + int dh = Math.Max(2, h * displayHeight / refHeight); + + // Clamp to display bounds + if (dx + dw > displayWidth) + { + dw = displayWidth - dx; + } + + if (dy + dh > displayHeight) + { + dh = displayHeight - dy; + } + + zoneList.Add((dx, dy, dw, dh, zoneId)); + + // Fill the grid for this zone + for (int r = dy; r < dy + dh && r < displayHeight; r++) + { + for (int c = dx; c < dx + dw && c < displayWidth; c++) + { + zoneGrid[r, c].Add(zoneId); + } + } + + zoneId++; + } + + // Draw top border + Console.Write(" +"); + Console.Write(new string('-', displayWidth)); + Console.WriteLine("+"); + + // Draw each row + for (int r = 0; r < displayHeight; r++) + { + Console.Write(" |"); + for (int c = 0; c < displayWidth; c++) + { + var zonesHere = zoneGrid[r, c]; + + if (zonesHere.Count == 0) + { + Console.Write(" "); + } + else + { + // Get the topmost zone at this position + int topZone = zonesHere[zonesHere.Count - 1]; + var rect = zoneList[topZone]; + + int x = rect.X; + int y = rect.Y; + int w = rect.Width; + int h = rect.Height; + + bool isTopEdge = r == y; + bool isBottomEdge = r == y + h - 1; + bool isLeftEdge = c == x; + bool isRightEdge = c == x + w - 1; + + // Draw borders + if ((isTopEdge || isBottomEdge) && (isLeftEdge || isRightEdge)) + { + Console.Write("+"); + } + else if (isTopEdge || isBottomEdge) + { + Console.Write("-"); + } + else if (isLeftEdge || isRightEdge) + { + Console.Write("|"); + } + else + { + // Use shading to show different zones + char[] shades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' }; + Console.Write(shades[topZone % shades.Length]); + } + } + } + + Console.WriteLine("|"); + } + + // Draw bottom border + Console.Write(" +"); + Console.Write(new string('-', displayWidth)); + Console.WriteLine("+"); + + // Draw legend + Console.WriteLine(); + Console.Write(" Legend: "); + char[] legendShades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' }; + for (int i = 0; i < Math.Min(zoneId, legendShades.Length); i++) + { + if (i > 0) + { + Console.Write(", "); + } + + Console.Write($"Zone {i} = {legendShades[i]}"); + } + + Console.WriteLine(); + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Models.cs b/src/modules/fancyzones/FancyZonesCLI/Models.cs new file mode 100644 index 0000000000..0c8bbefe54 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Models.cs @@ -0,0 +1,137 @@ +// 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 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 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 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 Hotkeys { get; set; } +} + +public sealed class LayoutHotkey +{ + [JsonPropertyName("key")] + public int Key { get; set; } + + [JsonPropertyName("layout-id")] + public string LayoutId { get; set; } = string.Empty; +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Program.cs b/src/modules/fancyzones/FancyZonesCLI/Program.cs new file mode 100644 index 0000000000..499abdc1c7 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Program.cs @@ -0,0 +1,764 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text.Json; + +namespace FancyZonesCLI; + +internal sealed class Program +{ + private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; + + private static int Main(string[] args) + { + // Initialize Windows messages + InitializeWindowMessages(); + + if (args.Length == 0) + { + PrintUsage(); + return 1; + } + + var command = args[0].ToLowerInvariant(); + + return command switch + { + "open-editor" or "editor" or "e" => OpenEditor(), + "get-monitors" or "monitors" or "m" => GetMonitors(), + "get-layouts" or "layouts" or "ls" => GetLayouts(), + "get-active-layout" or "active" or "get-active" or "a" => GetActiveLayout(), + "set-layout" or "set" or "s" => args.Length >= 2 ? SetLayout(args.Skip(1).ToArray()) : PrintErrorAndReturn("Error: set-layout requires a UUID parameter"), + "open-settings" or "settings" => OpenSettings(), + "get-hotkeys" or "hotkeys" or "hk" => GetHotkeys(), + "set-hotkey" or "shk" => args.Length >= 3 ? SetHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), args[2]) : PrintErrorAndReturn("Error: set-hotkey requires "), + "remove-hotkey" or "rhk" => args.Length >= 2 ? RemoveHotkey(int.Parse(args[1], CultureInfo.InvariantCulture)) : PrintErrorAndReturn("Error: remove-hotkey requires "), + "help" or "--help" or "-h" => PrintUsageAndReturn(), + _ => PrintUnknownCommandAndReturn(command), + }; + } + + private static int PrintErrorAndReturn(string message) + { + Console.WriteLine(message); + return 1; + } + + private static int PrintUsageAndReturn() + { + PrintUsage(); + return 0; + } + + private static int PrintUnknownCommandAndReturn(string command) + { + Console.WriteLine($"Error: Unknown command: {command}\n"); + PrintUsage(); + return 1; + } + + private static void PrintUsage() + { + Console.WriteLine("FancyZones CLI - Command line interface for FancyZones"); + Console.WriteLine("======================================================"); + Console.WriteLine(); + Console.WriteLine("Usage: FancyZonesCLI.exe [options]"); + Console.WriteLine(); + Console.WriteLine("Commands:"); + Console.WriteLine(" open-editor (editor, e) Launch FancyZones layout editor"); + Console.WriteLine(" get-monitors (monitors, m) List all monitors and their properties"); + Console.WriteLine(" get-layouts (layouts, ls) List all available layouts"); + Console.WriteLine(" get-active-layout (active, a) Show currently active layout"); + Console.WriteLine(" set-layout (set, s) [options]"); + Console.WriteLine(" Set layout by UUID"); + Console.WriteLine(" --monitor Apply to monitor N (1-based)"); + Console.WriteLine(" --all Apply to all monitors"); + Console.WriteLine(" open-settings (settings) Open FancyZones settings page"); + Console.WriteLine(" get-hotkeys (hotkeys, hk) List all layout hotkeys"); + Console.WriteLine(" set-hotkey (shk) Assign hotkey (0-9) to CUSTOM layout"); + Console.WriteLine(" Note: Only custom layouts work with hotkeys"); + Console.WriteLine(" remove-hotkey (rhk) Remove hotkey assignment"); + Console.WriteLine(" help Show this help message"); + Console.WriteLine(); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" FancyZonesCLI.exe e # Open editor (short)"); + Console.WriteLine(" FancyZonesCLI.exe m # List monitors (short)"); + Console.WriteLine(" FancyZonesCLI.exe ls # List layouts (short)"); + Console.WriteLine(" FancyZonesCLI.exe a # Get active layout (short)"); + Console.WriteLine(" FancyZonesCLI.exe s focus --all # Set layout (short)"); + Console.WriteLine(" FancyZonesCLI.exe open-editor # Open editor (long)"); + Console.WriteLine(" FancyZonesCLI.exe get-monitors"); + Console.WriteLine(" FancyZonesCLI.exe get-layouts"); + Console.WriteLine(" FancyZonesCLI.exe set-layout {12345678-1234-1234-1234-123456789012}"); + Console.WriteLine(" FancyZonesCLI.exe set-layout focus --monitor 2"); + Console.WriteLine(" FancyZonesCLI.exe set-layout columns --all"); + Console.WriteLine(" FancyZonesCLI.exe set-hotkey 3 {12345678-1234-1234-1234-123456789012}"); + } + + private static int OpenEditor() + { + var editorExe = "PowerToys.FancyZonesEditor.exe"; + + // Check if editor-parameters.json exists + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + var editorParamsPath = Path.Combine(localAppData, "Microsoft", "PowerToys", "FancyZones", "editor-parameters.json"); + + if (!File.Exists(editorParamsPath)) + { + Console.WriteLine("Error: editor-parameters.json not found."); + Console.WriteLine("Please launch FancyZones Editor using Win+` (Win+Backtick) hotkey first."); + return 1; + } + + // Check if editor is already running + var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault(); + if (existingProcess != null) + { + NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle); + Console.WriteLine("FancyZones Editor is already running. Brought window to foreground."); + return 0; + } + + // 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, + }); + Console.WriteLine("FancyZones Editor launched successfully."); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Failed to launch: {ex.Message}"); + return 1; + } + } + + Console.WriteLine($"Error: Could not find {editorExe} in {AppContext.BaseDirectory}"); + return 1; + } + + private static int 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) + { + Console.WriteLine("Error: PowerToys.exe not found. Please ensure PowerToys is installed."); + return 1; + } + + Process.Start(new ProcessStartInfo + { + FileName = powertoysExe, + Arguments = "--open-settings=FancyZones", + UseShellExecute = false, + }); + Console.WriteLine("FancyZones Settings opened successfully."); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: Failed to open FancyZones Settings. {ex.Message}"); + return 1; + } + } + + private static int GetMonitors() + { + var dataPath = GetFancyZonesDataPath(); + var appliedLayoutsPath = Path.Combine(dataPath, "applied-layouts.json"); + + if (!File.Exists(appliedLayoutsPath)) + { + Console.WriteLine("Error: applied-layouts.json not found."); + return 1; + } + + try + { + var json = File.ReadAllText(appliedLayoutsPath); + var appliedLayouts = JsonSerializer.Deserialize(json, FancyZonesJsonContext.Default.AppliedLayouts); + + if (appliedLayouts?.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + Console.WriteLine("No monitors found."); + return 0; + } + + Console.WriteLine($"=== Monitors ({appliedLayouts.Layouts.Count} total) ==="); + Console.WriteLine(); + + for (int i = 0; i < appliedLayouts.Layouts.Count; i++) + { + var layout = appliedLayouts.Layouts[i]; + var monitorNum = i + 1; + + Console.WriteLine($"Monitor {monitorNum}:"); + Console.WriteLine($" Monitor: {layout.Device.Monitor}"); + Console.WriteLine($" Monitor Instance: {layout.Device.MonitorInstance}"); + Console.WriteLine($" Monitor Number: {layout.Device.MonitorNumber}"); + Console.WriteLine($" Serial Number: {layout.Device.SerialNumber}"); + Console.WriteLine($" Virtual Desktop: {layout.Device.VirtualDesktop}"); + Console.WriteLine($" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); + Console.WriteLine($" Active Layout: {layout.AppliedLayout.Type}"); + Console.WriteLine($" Zone Count: {layout.AppliedLayout.ZoneCount}"); + Console.WriteLine(); + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: Failed to read monitor information. {ex.Message}"); + return 1; + } + } + + private static int GetLayouts() + { + var dataPath = GetFancyZonesDataPath(); + var templatesPath = Path.Combine(dataPath, "layout-templates.json"); + var customLayoutsPath = Path.Combine(dataPath, "custom-layouts.json"); + + // Print template layouts + if (File.Exists(templatesPath)) + { + try + { + var templatesJson = JsonSerializer.Deserialize(File.ReadAllText(templatesPath), FancyZonesJsonContext.Default.LayoutTemplates); + if (templatesJson?.Templates != null) + { + Console.WriteLine($"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n"); + + for (int i = 0; i < templatesJson.Templates.Count; i++) + { + var template = templatesJson.Templates[i]; + Console.WriteLine($"[T{i + 1}] {template.Type}"); + Console.WriteLine($" Zones: {template.ZoneCount}"); + if (template.ShowSpacing && template.Spacing > 0) + { + Console.WriteLine($", Spacing: {template.Spacing}px"); + } + + Console.WriteLine(); + + // Draw visual preview + LayoutVisualizer.DrawTemplateLayout(template); + + if (i < templatesJson.Templates.Count - 1) + { + Console.WriteLine(); + } + } + + Console.WriteLine("\n"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error parsing templates: {ex.Message}"); + } + } + + // Print custom layouts + if (File.Exists(customLayoutsPath)) + { + try + { + var customLayouts = JsonSerializer.Deserialize(File.ReadAllText(customLayoutsPath), FancyZonesJsonContext.Default.CustomLayouts); + if (customLayouts?.Layouts != null) + { + Console.WriteLine($"=== Custom Layouts ({customLayouts.Layouts.Count} total) ==="); + + for (int i = 0; i < customLayouts.Layouts.Count; i++) + { + var layout = customLayouts.Layouts[i]; + Console.WriteLine($"[{i + 1}] {layout.Name}"); + Console.WriteLine($" UUID: {layout.Uuid}"); + Console.Write($" 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)) + { + Console.Write($" ({rows.GetInt32()}x{cols.GetInt32()} grid)"); + } + else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones)) + { + Console.Write($" ({zones.GetArrayLength()} zones)"); + isCanvasLayout = true; + } + } + + Console.WriteLine("\n"); + + // Draw visual preview + LayoutVisualizer.DrawCustomLayout(layout); + + // Add note for canvas layouts + if (isCanvasLayout) + { + Console.WriteLine("\n Note: Canvas layout preview is approximate."); + Console.WriteLine(" Open FancyZones Editor for precise zone boundaries."); + } + + if (i < customLayouts.Layouts.Count - 1) + { + Console.WriteLine(); + } + } + + Console.WriteLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error parsing custom layouts: {ex.Message}"); + } + } + + return 0; + } + + private static int GetActiveLayout() + { + var dataPath = GetFancyZonesDataPath(); + var appliedLayoutsPath = Path.Combine(dataPath, "applied-layouts.json"); + + if (!File.Exists(appliedLayoutsPath)) + { + Console.WriteLine($"Error: Could not find applied-layouts.json"); + return 1; + } + + try + { + var appliedLayouts = JsonSerializer.Deserialize(File.ReadAllText(appliedLayoutsPath), FancyZonesJsonContext.Default.AppliedLayouts); + if (appliedLayouts?.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + Console.WriteLine("No active layouts found."); + return 0; + } + + Console.WriteLine("\n=== Active FancyZones Layout(s) ===\n"); + + for (int i = 0; i < appliedLayouts.Layouts.Count; i++) + { + var layout = appliedLayouts.Layouts[i]; + Console.WriteLine($"Monitor {i + 1}:"); + Console.WriteLine($" Name: {layout.AppliedLayout.Type}"); + Console.WriteLine($" UUID: {layout.AppliedLayout.Uuid}"); + Console.WriteLine($" Type: {layout.AppliedLayout.Type} ({layout.AppliedLayout.ZoneCount} zones)"); + + if (layout.AppliedLayout.ShowSpacing) + { + Console.WriteLine($" Spacing: {layout.AppliedLayout.Spacing}px"); + } + + Console.WriteLine($" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); + + if (i < appliedLayouts.Layouts.Count - 1) + { + Console.WriteLine(); + } + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static int SetLayout(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("Error: set-layout requires a UUID parameter"); + return 1; + } + + 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 + { + Console.WriteLine($"Error: Invalid monitor number: {args[i + 1]}"); + return 1; + } + } + else if (args[i] == "--all") + { + applyToAll = true; + } + } + + if (targetMonitor.HasValue && applyToAll) + { + Console.WriteLine("Error: Cannot specify both --monitor and --all"); + return 1; + } + + var dataPath = GetFancyZonesDataPath(); + var appliedLayoutsPath = Path.Combine(dataPath, "applied-layouts.json"); + var customLayoutsPath = Path.Combine(dataPath, "custom-layouts.json"); + var templatesPath = Path.Combine(dataPath, "layout-templates.json"); + + if (!File.Exists(appliedLayoutsPath)) + { + Console.WriteLine("Error: applied-layouts.json not found"); + return 1; + } + + try + { + // Try to find layout in custom layouts first (by UUID) + CustomLayout targetCustomLayout = null; + TemplateLayout targetTemplate = null; + + if (File.Exists(customLayoutsPath)) + { + var customLayouts = JsonSerializer.Deserialize(File.ReadAllText(customLayoutsPath), FancyZonesJsonContext.Default.CustomLayouts); + targetCustomLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(uuid, StringComparison.OrdinalIgnoreCase)); + } + + // If not found in custom layouts, try template layouts (by type name or UUID) + if (targetCustomLayout == null && File.Exists(templatesPath)) + { + var templates = JsonSerializer.Deserialize(File.ReadAllText(templatesPath), FancyZonesJsonContext.Default.LayoutTemplates); + + // Try matching by type name (case-insensitive) + targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase)); + } + + if (targetCustomLayout == null && targetTemplate == null) + { + Console.WriteLine($"Error: Layout '{uuid}' not found"); + Console.WriteLine("Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')"); + Console.WriteLine(" For custom layouts, use the UUID from 'get-layouts'"); + return 1; + } + + // Read current applied layouts + var appliedLayouts = JsonSerializer.Deserialize(File.ReadAllText(appliedLayoutsPath), FancyZonesJsonContext.Default.AppliedLayouts); + if (appliedLayouts?.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + Console.WriteLine("Error: No monitors configured"); + return 1; + } + + // Determine which monitors to update + List monitorsToUpdate = new List(); + 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) + { + Console.WriteLine($"Error: Monitor {targetMonitor.Value} not found. Available monitors: 1-{appliedLayouts.Layouts.Count}"); + return 1; + } + + 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 + File.WriteAllText(appliedLayoutsPath, JsonSerializer.Serialize(appliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts)); + + // Notify FancyZones to reload + NotifyFancyZones(wmPrivAppliedLayoutsFileUpdate); + + string layoutName = targetCustomLayout?.Name ?? targetTemplate?.Type ?? uuid; + if (applyToAll) + { + Console.WriteLine($"Layout '{layoutName}' applied to all {monitorsToUpdate.Count} monitors"); + } + else if (targetMonitor.HasValue) + { + Console.WriteLine($"Layout '{layoutName}' applied to monitor {targetMonitor.Value}"); + } + else + { + Console.WriteLine($"Layout '{layoutName}' applied to monitor 1"); + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static int GetHotkeys() + { + var dataPath = GetFancyZonesDataPath(); + var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); + + if (!File.Exists(hotkeysPath)) + { + Console.WriteLine("No hotkeys configured."); + return 0; + } + + try + { + var hotkeys = JsonSerializer.Deserialize(File.ReadAllText(hotkeysPath), FancyZonesJsonContext.Default.LayoutHotkeys); + if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0) + { + Console.WriteLine("No hotkeys configured."); + return 0; + } + + Console.WriteLine("=== Layout Hotkeys ===\n"); + Console.WriteLine("Press Win + Ctrl + Alt + to switch layouts:\n"); + + foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key)) + { + Console.WriteLine($" [{hotkey.Key}] → {hotkey.LayoutId}"); + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static int SetHotkey(int key, string layoutUuid) + { + if (key < 0 || key > 9) + { + Console.WriteLine("Error: Key must be between 0 and 9"); + return 1; + } + + // Check if this is a custom layout UUID + var dataPath = GetFancyZonesDataPath(); + var customLayoutsPath = Path.Combine(dataPath, "custom-layouts.json"); + bool isCustomLayout = false; + string layoutName = layoutUuid; + + if (File.Exists(customLayoutsPath)) + { + try + { + var customLayoutsJson = File.ReadAllText(customLayoutsPath); + var customLayouts = JsonSerializer.Deserialize(customLayoutsJson, FancyZonesJsonContext.Default.CustomLayouts); + var layout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(layoutUuid, StringComparison.OrdinalIgnoreCase)); + if (layout != null) + { + isCustomLayout = true; + layoutName = layout.Name; + } + } + catch + { + // Ignore parse errors + } + } + + var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); + + try + { + LayoutHotkeys hotkeys; + if (File.Exists(hotkeysPath)) + { + hotkeys = JsonSerializer.Deserialize(File.ReadAllText(hotkeysPath), FancyZonesJsonContext.Default.LayoutHotkeys) ?? new LayoutHotkeys(); + } + else + { + hotkeys = new LayoutHotkeys(); + } + + hotkeys.Hotkeys ??= new List(); + + // 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 + File.WriteAllText(hotkeysPath, JsonSerializer.Serialize(hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys)); + + // Notify FancyZones + NotifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + if (isCustomLayout) + { + Console.WriteLine($"✓ Hotkey {key} assigned to custom layout '{layoutName}'"); + Console.WriteLine($" Press Win + Ctrl + Alt + {key} to switch to this layout"); + } + else + { + Console.WriteLine($"⚠ Warning: Hotkey {key} assigned to '{layoutUuid}'"); + Console.WriteLine($" Note: FancyZones hotkeys only work with CUSTOM layouts."); + Console.WriteLine($" Template layouts (focus, columns, rows, etc.) cannot be used with hotkeys."); + Console.WriteLine($" Create a custom layout in the FancyZones Editor to use this hotkey."); + } + + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static int RemoveHotkey(int key) + { + var dataPath = GetFancyZonesDataPath(); + var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); + + if (!File.Exists(hotkeysPath)) + { + Console.WriteLine($"No hotkey assigned to key {key}"); + return 0; + } + + try + { + var hotkeys = JsonSerializer.Deserialize(File.ReadAllText(hotkeysPath), FancyZonesJsonContext.Default.LayoutHotkeys); + if (hotkeys?.Hotkeys == null) + { + Console.WriteLine($"No hotkey assigned to key {key}"); + return 0; + } + + var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key); + if (removed == 0) + { + Console.WriteLine($"No hotkey assigned to key {key}"); + return 0; + } + + // Save + File.WriteAllText(hotkeysPath, JsonSerializer.Serialize(hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys)); + + // Notify FancyZones + NotifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + Console.WriteLine($"Hotkey {key} removed"); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return 1; + } + } + + private static string GetFancyZonesDataPath() + { + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return Path.Combine(localAppData, "Microsoft", "PowerToys", "FancyZones"); + } + + // Windows Messages for notifying FancyZones + private static uint wmPrivAppliedLayoutsFileUpdate; + private static uint wmPrivLayoutHotkeysFileUpdate; + + private static void NotifyFancyZones(uint message) + { + // Broadcast message to all windows + NativeMethods.PostMessage(NativeMethods.HWND_BROADCAST, message, IntPtr.Zero, IntPtr.Zero); + } + + private static void InitializeWindowMessages() + { + wmPrivAppliedLayoutsFileUpdate = NativeMethods.RegisterWindowMessage("{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}"); + wmPrivLayoutHotkeysFileUpdate = NativeMethods.RegisterWindowMessage("{07229b7e-4f22-4357-b136-33c289be2295}"); + } +} + +internal static class NativeMethods +{ + public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); + + [DllImport("user32.dll", SetLastError = true)] + public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern uint RegisterWindowMessage(string lpString); +}