From b4cdec5e5d9ed82c187f776bdabe5ede3e5b2f1f Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Fri, 5 Dec 2025 13:12:43 +0800 Subject: [PATCH] use common --- .../FancyZonesCLI/FancyZonesData.cs | 133 +++ .../FancyZonesCLI/FancyZonesPaths.cs | 30 + .../FancyZonesCLI/LayoutVisualizer.cs | 363 +++----- .../fancyzones/FancyZonesCLI/Program.cs | 845 +++++++----------- 4 files changed, 636 insertions(+), 735 deletions(-) create mode 100644 src/modules/fancyzones/FancyZonesCLI/FancyZonesData.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/FancyZonesPaths.cs diff --git a/src/modules/fancyzones/FancyZonesCLI/FancyZonesData.cs b/src/modules/fancyzones/FancyZonesCLI/FancyZonesData.cs new file mode 100644 index 0000000000..e5515c7537 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/FancyZonesData.cs @@ -0,0 +1,133 @@ +// 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; + +/// +/// Provides methods to read and write FancyZones configuration data. +/// +internal static class FancyZonesData +{ + /// + /// Try to read applied layouts configuration. + /// + public static bool TryReadAppliedLayouts(out AppliedLayouts result, out string error) + { + return TryReadJsonFile(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts, out result, out error); + } + + /// + /// Read applied layouts or return null if not found. + /// + public static AppliedLayouts ReadAppliedLayouts() + { + return ReadJsonFileOrDefault(FancyZonesPaths.AppliedLayouts, FancyZonesJsonContext.Default.AppliedLayouts); + } + + /// + /// Write applied layouts configuration. + /// + public static void WriteAppliedLayouts(AppliedLayouts layouts) + { + WriteJsonFile(FancyZonesPaths.AppliedLayouts, layouts, FancyZonesJsonContext.Default.AppliedLayouts); + } + + /// + /// Read custom layouts or return null if not found. + /// + public static CustomLayouts ReadCustomLayouts() + { + return ReadJsonFileOrDefault(FancyZonesPaths.CustomLayouts, FancyZonesJsonContext.Default.CustomLayouts); + } + + /// + /// Read layout templates or return null if not found. + /// + public static LayoutTemplates ReadLayoutTemplates() + { + return ReadJsonFileOrDefault(FancyZonesPaths.LayoutTemplates, FancyZonesJsonContext.Default.LayoutTemplates); + } + + /// + /// Read layout hotkeys or return null if not found. + /// + public static LayoutHotkeys ReadLayoutHotkeys() + { + return ReadJsonFileOrDefault(FancyZonesPaths.LayoutHotkeys, FancyZonesJsonContext.Default.LayoutHotkeys); + } + + /// + /// Write layout hotkeys configuration. + /// + public static void WriteLayoutHotkeys(LayoutHotkeys hotkeys) + { + WriteJsonFile(FancyZonesPaths.LayoutHotkeys, hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys); + } + + /// + /// Check if editor parameters file exists. + /// + public static bool EditorParametersExist() + { + return File.Exists(FancyZonesPaths.EditorParameters); + } + + private static bool TryReadJsonFile(string filePath, JsonTypeInfo jsonTypeInfo, out T result, out string error) + where T : class + { + result = null; + error = null; + + if (!File.Exists(filePath)) + { + error = $"File not found: {Path.GetFileName(filePath)}"; + return false; + } + + try + { + var json = File.ReadAllText(filePath); + result = JsonSerializer.Deserialize(json, jsonTypeInfo); + if (result == null) + { + error = $"Failed to parse {Path.GetFileName(filePath)}"; + return false; + } + + return true; + } + catch (JsonException ex) + { + error = $"JSON parse error in {Path.GetFileName(filePath)}: {ex.Message}"; + return false; + } + catch (IOException ex) + { + error = $"Failed to read {Path.GetFileName(filePath)}: {ex.Message}"; + return false; + } + } + + private static T ReadJsonFileOrDefault(string filePath, JsonTypeInfo jsonTypeInfo, T defaultValue = null) + where T : class + { + if (TryReadJsonFile(filePath, jsonTypeInfo, out var result, out _)) + { + return result; + } + + return defaultValue; + } + + private static void WriteJsonFile(string filePath, T data, JsonTypeInfo jsonTypeInfo) + { + var json = JsonSerializer.Serialize(data, jsonTypeInfo); + File.WriteAllText(filePath, json); + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/FancyZonesPaths.cs b/src/modules/fancyzones/FancyZonesCLI/FancyZonesPaths.cs new file mode 100644 index 0000000000..f04d375392 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/FancyZonesPaths.cs @@ -0,0 +1,30 @@ +// 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; + +namespace FancyZonesCLI; + +/// +/// Provides paths to FancyZones configuration files. +/// +internal static class FancyZonesPaths +{ + private static readonly string DataPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "PowerToys", + "FancyZones"); + + public static string AppliedLayouts => Path.Combine(DataPath, "applied-layouts.json"); + + public static string CustomLayouts => Path.Combine(DataPath, "custom-layouts.json"); + + public static string LayoutTemplates => Path.Combine(DataPath, "layout-templates.json"); + + public static string LayoutHotkeys => Path.Combine(DataPath, "layout-hotkeys.json"); + + public static string EditorParameters => Path.Combine(DataPath, "editor-parameters.json"); +} diff --git a/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs b/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs index 80696b7528..fecdf33dbe 100644 --- a/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs +++ b/src/modules/fancyzones/FancyZonesCLI/LayoutVisualizer.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Globalization; using System.Text; using System.Text.Json; @@ -12,20 +12,21 @@ namespace FancyZonesCLI; public static class LayoutVisualizer { - public static void DrawTemplateLayout(TemplateLayout template) + public static string DrawTemplateLayout(TemplateLayout template) { - Console.WriteLine(" Visual Preview:"); + var sb = new StringBuilder(); + sb.AppendLine(" Visual Preview:"); switch (template.Type.ToLowerInvariant()) { case "focus": - DrawFocusLayout(template.ZoneCount > 0 ? template.ZoneCount : 3); + sb.Append(RenderFocusLayout(template.ZoneCount > 0 ? template.ZoneCount : 3)); break; case "columns": - DrawGridLayout(1, template.ZoneCount > 0 ? template.ZoneCount : 3); + sb.Append(RenderGridLayout(1, template.ZoneCount > 0 ? template.ZoneCount : 3)); break; case "rows": - DrawGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3, 1); + sb.Append(RenderGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3, 1)); break; case "grid": // Grid layout: calculate rows and columns from zone count @@ -45,28 +46,31 @@ public static class LayoutVisualizer cols++; } - DrawGridLayoutWithZoneCount(rows, cols, zoneCount); + sb.Append(RenderGridLayoutWithZoneCount(rows, cols, zoneCount)); break; case "priority-grid": - DrawPriorityGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3); + sb.Append(RenderPriorityGridLayout(template.ZoneCount > 0 ? template.ZoneCount : 3)); break; case "blank": - Console.WriteLine(" (No zones)"); + sb.AppendLine(" (No zones)"); break; default: - Console.WriteLine($" ({template.Type} layout)"); + sb.AppendLine(CultureInfo.InvariantCulture, $" ({template.Type} layout)"); break; } + + return sb.ToString(); } - public static void DrawCustomLayout(CustomLayout layout) + public static string DrawCustomLayout(CustomLayout layout) { if (layout.Info.ValueKind == JsonValueKind.Undefined || layout.Info.ValueKind == JsonValueKind.Null) { - return; + return string.Empty; } - Console.WriteLine(" Visual Preview:"); + var sb = new StringBuilder(); + sb.AppendLine(" Visual Preview:"); if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && @@ -78,12 +82,12 @@ public static class LayoutVisualizer // Check if there's a cell-child-map (merged cells) if (layout.Info.TryGetProperty("cell-child-map", out var cellMap)) { - DrawGridLayoutWithMergedCells(r, c, cellMap); + sb.Append(RenderGridLayoutWithMergedCells(r, c, cellMap)); } else { int height = r >= 4 ? 12 : 8; - DrawGridLayout(r, c, 30, height); + sb.Append(RenderGridLayout(r, c, 30, height)); } } else if (layout.Type == "canvas" && @@ -91,60 +95,61 @@ public static class LayoutVisualizer layout.Info.TryGetProperty("ref-width", out var refWidth) && layout.Info.TryGetProperty("ref-height", out var refHeight)) { - DrawCanvasLayout(zones, refWidth.GetInt32(), refHeight.GetInt32()); + sb.Append(RenderCanvasLayout(zones, refWidth.GetInt32(), refHeight.GetInt32())); } + + return sb.ToString(); } - private static void DrawFocusLayout(int zoneCount = 3) + private static string RenderFocusLayout(int zoneCount = 3) { + var sb = new StringBuilder(); + // 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(" +-------+"); + sb.AppendLine(" +-------+"); + sb.AppendLine(" | |"); + sb.AppendLine(" | |"); + sb.AppendLine(" +-------+"); } else if (zoneCount == 2) { - Console.WriteLine(" +-------+"); - Console.WriteLine(" | |"); - Console.WriteLine(" | +-------+"); - Console.WriteLine(" +-| |"); - Console.WriteLine(" | |"); - Console.WriteLine(" +-------+"); + sb.AppendLine(" +-------+"); + sb.AppendLine(" | |"); + sb.AppendLine(" | +-------+"); + sb.AppendLine(" +-| |"); + sb.AppendLine(" | |"); + sb.AppendLine(" +-------+"); } 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(" +-------+"); + sb.AppendLine(" +-------+"); + sb.AppendLine(" | |"); + sb.AppendLine(" | +-------+"); + sb.AppendLine(" +-| |"); + sb.AppendLine(" | +-------+"); + sb.AppendLine(" +-| |"); + sb.AppendLine(" ..."); + sb.AppendLine(CultureInfo.InvariantCulture, $" (total: {zoneCount} zones)"); + sb.AppendLine(" ..."); + sb.AppendLine(" | +-------+"); + sb.AppendLine(" +-| |"); + sb.AppendLine(" | |"); + sb.AppendLine(" +-------+"); } + + return sb.ToString(); } - private static void DrawPriorityGridLayout(int zoneCount = 3) + private static string RenderPriorityGridLayout(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); + return RenderGridLayoutWithCellMap(cellMap); } else { @@ -162,7 +167,7 @@ public static class LayoutVisualizer cols++; } - DrawGridLayoutWithZoneCount(rows, cols, zoneCount); + return RenderGridLayoutWithZoneCount(rows, cols, zoneCount); } } @@ -186,8 +191,9 @@ public static class LayoutVisualizer }; } - private static void DrawGridLayoutWithCellMap(int[,] cellMap, int width = 30, int height = 8) + private static string RenderGridLayoutWithCellMap(int[,] cellMap, int width = 30, int height = 8) { + var sb = new StringBuilder(); int rows = cellMap.GetLength(0); int cols = cellMap.GetLength(1); @@ -197,79 +203,54 @@ public static class LayoutVisualizer for (int r = 0; r < rows; r++) { // Top border - Console.Write(" +"); + sb.Append(" +"); 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("+"); - } + sb.Append(mergeLeft ? new string(' ', cellWidth) : new string(' ', cellWidth - 1) + "+"); } else { - if (mergeLeft) - { - Console.Write(new string('-', cellWidth)); - } - else - { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); - } + sb.Append(mergeLeft ? new string('-', cellWidth) : new string('-', cellWidth - 1) + "+"); } } - Console.WriteLine(); + sb.AppendLine(); // Cell content for (int h = 0; h < cellHeight - 1; h++) { - Console.Write(" "); + sb.Append(" "); 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)); + sb.Append(mergeLeft ? ' ' : '|'); + sb.Append(' ', cellWidth - 1); } - Console.WriteLine("|"); + sb.AppendLine("|"); } } // Bottom border - Console.Write(" +"); + sb.Append(" +"); for (int c = 0; c < cols; c++) { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); + sb.Append('-', cellWidth - 1); + sb.Append('+'); } - Console.WriteLine(); + sb.AppendLine(); + return sb.ToString(); } - private static void DrawGridLayoutWithMergedCells(int rows, int cols, JsonElement cellMap) + private static string RenderGridLayoutWithMergedCells(int rows, int cols, JsonElement cellMap) { + var sb = new StringBuilder(); const int displayWidth = 39; const int displayHeight = 12; @@ -284,45 +265,20 @@ public static class LayoutVisualizer } } - // 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("+"); + sb.Append(" +"); + sb.Append('-', displayWidth); + sb.AppendLine("+"); // 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(" |"); + sb.Append(" |"); for (int c = 0; c < cols; c++) { @@ -330,47 +286,35 @@ public static class LayoutVisualizer 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; - } + bool zoneHasTopBorder = r > 0 && h == 0 && currentZone != zoneMap[r - 1, c]; - // Draw left border if needed if (needLeftBorder) { - Console.Write("|"); - - // Fill rest of cell - for (int w = 1; w < cellWidth; w++) - { - Console.Write(zoneHasTopBorder ? "-" : " "); - } + sb.Append('|'); + sb.Append(zoneHasTopBorder ? '-' : ' ', cellWidth - 1); } else { - // No left border, fill entire cell - for (int w = 0; w < cellWidth; w++) - { - Console.Write(zoneHasTopBorder ? "-" : " "); - } + sb.Append(zoneHasTopBorder ? '-' : ' ', cellWidth); } } - Console.WriteLine("|"); + sb.AppendLine("|"); } } // Draw bottom border - Console.Write(" +"); - Console.Write(new string('-', displayWidth)); - Console.WriteLine("+"); + sb.Append(" +"); + sb.Append('-', displayWidth); + sb.AppendLine("+"); + + return sb.ToString(); } - public static void DrawGridLayoutWithZoneCount(int rows, int cols, int zoneCount, int width = 30, int height = 8) + public static string RenderGridLayoutWithZoneCount(int rows, int cols, int zoneCount, int width = 30, int height = 8) { + var sb = new StringBuilder(); + // Build zone map like Editor's InitGrid int[,] zoneMap = new int[rows, cols]; int index = 0; @@ -392,103 +336,93 @@ public static class LayoutVisualizer for (int r = 0; r < rows; r++) { // Top border - Console.Write(" +"); + sb.Append(" +"); 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) + sb.Append('-', mergeLeft ? cellWidth : cellWidth - 1); + if (!mergeLeft) { - Console.Write(new string('-', cellWidth)); - } - else - { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); + sb.Append('+'); } } - Console.WriteLine(); + sb.AppendLine(); // Cell content for (int h = 0; h < cellHeight - 1; h++) { - Console.Write(" "); + sb.Append(" "); 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)); + sb.Append(mergeLeft ? ' ' : '|'); + sb.Append(' ', cellWidth - 1); } - Console.WriteLine("|"); + sb.AppendLine("|"); } } // Bottom border - Console.Write(" +"); + sb.Append(" +"); for (int c = 0; c < cols; c++) { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); + sb.Append('-', cellWidth - 1); + sb.Append('+'); } - Console.WriteLine(); + sb.AppendLine(); + return sb.ToString(); } - public static void DrawGridLayout(int rows, int cols, int width = 30, int height = 8) + public static string RenderGridLayout(int rows, int cols, int width = 30, int height = 8) { + var sb = new StringBuilder(); int cellWidth = width / cols; int cellHeight = height / rows; for (int r = 0; r < rows; r++) { // Top border - Console.Write(" +"); + sb.Append(" +"); for (int c = 0; c < cols; c++) { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); + sb.Append('-', cellWidth - 1); + sb.Append('+'); } - Console.WriteLine(); + sb.AppendLine(); // Cell content for (int h = 0; h < cellHeight - 1; h++) { - Console.Write(" "); + sb.Append(" "); for (int c = 0; c < cols; c++) { - Console.Write("|"); - Console.Write(new string(' ', cellWidth - 1)); + sb.Append('|'); + sb.Append(' ', cellWidth - 1); } - Console.WriteLine("|"); + sb.AppendLine("|"); } } // Bottom border - Console.Write(" +"); + sb.Append(" +"); for (int c = 0; c < cols; c++) { - Console.Write(new string('-', cellWidth - 1)); - Console.Write("+"); + sb.Append('-', cellWidth - 1); + sb.Append('+'); } - Console.WriteLine(); + sb.AppendLine(); + return sb.ToString(); } - private static void DrawCanvasLayout(JsonElement zones, int refWidth, int refHeight) + private static string RenderCanvasLayout(JsonElement zones, int refWidth, int refHeight) { + var sb = new StringBuilder(); const int displayWidth = 49; const int displayHeight = 15; @@ -518,7 +452,6 @@ public static class LayoutVisualizer 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; @@ -531,7 +464,6 @@ public static class LayoutVisualizer 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++) @@ -544,82 +476,75 @@ public static class LayoutVisualizer } // Draw top border - Console.Write(" +"); - Console.Write(new string('-', displayWidth)); - Console.WriteLine("+"); + sb.Append(" +"); + sb.Append('-', displayWidth); + sb.AppendLine("+"); // Draw each row + char[] shades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' }; + for (int r = 0; r < displayHeight; r++) { - Console.Write(" |"); + sb.Append(" |"); for (int c = 0; c < displayWidth; c++) { var zonesHere = zoneGrid[r, c]; if (zonesHere.Count == 0) { - Console.Write(" "); + sb.Append(' '); } 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 == rect.Y; + bool isBottomEdge = r == rect.Y + rect.Height - 1; + bool isLeftEdge = c == rect.X; + bool isRightEdge = c == rect.X + rect.Width - 1; - 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("+"); + sb.Append('+'); } else if (isTopEdge || isBottomEdge) { - Console.Write("-"); + sb.Append('-'); } else if (isLeftEdge || isRightEdge) { - Console.Write("|"); + sb.Append('|'); } else { - // Use shading to show different zones - char[] shades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' }; - Console.Write(shades[topZone % shades.Length]); + sb.Append(shades[topZone % shades.Length]); } } } - Console.WriteLine("|"); + sb.AppendLine("|"); } // Draw bottom border - Console.Write(" +"); - Console.Write(new string('-', displayWidth)); - Console.WriteLine("+"); + sb.Append(" +"); + sb.Append('-', displayWidth); + sb.AppendLine("+"); // Draw legend - Console.WriteLine(); - Console.Write(" Legend: "); - char[] legendShades = { '.', ':', '░', '▒', '▓', '█', '◆', '●', '■', '▪' }; - for (int i = 0; i < Math.Min(zoneId, legendShades.Length); i++) + sb.AppendLine(); + sb.Append(" Legend: "); + for (int i = 0; i < Math.Min(zoneId, shades.Length); i++) { if (i > 0) { - Console.Write(", "); + sb.Append(", "); } - Console.Write($"Zone {i} = {legendShades[i]}"); + sb.Append(CultureInfo.InvariantCulture, $"Zone {i} = {shades[i]}"); } - Console.WriteLine(); + sb.AppendLine(); + return sb.ToString(); } } diff --git a/src/modules/fancyzones/FancyZonesCLI/Program.cs b/src/modules/fancyzones/FancyZonesCLI/Program.cs index 499abdc1c7..d8781392f3 100644 --- a/src/modules/fancyzones/FancyZonesCLI/Program.cs +++ b/src/modules/fancyzones/FancyZonesCLI/Program.cs @@ -15,108 +15,96 @@ namespace FancyZonesCLI; internal sealed class Program { - private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; - private static int Main(string[] args) { // Initialize Windows messages InitializeWindowMessages(); + (int ExitCode, string Output) result; + if (args.Length == 0) { - PrintUsage(); - return 1; + result = (1, GetUsageText()); + } + else + { + var command = args[0].ToLowerInvariant(); + + result = 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()) : (1, "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]) : (1, "Error: set-hotkey requires "), + "remove-hotkey" or "rhk" => args.Length >= 2 ? RemoveHotkey(int.Parse(args[1], CultureInfo.InvariantCulture)) : (1, "Error: remove-hotkey requires "), + "help" or "--help" or "-h" => (0, GetUsageText()), + _ => (1, $"Error: Unknown command: {command}\n\n{GetUsageText()}"), + }; } - var command = args[0].ToLowerInvariant(); - - return command switch + // Output result + if (!string.IsNullOrEmpty(result.Output)) { - "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), - }; + Console.WriteLine(result.Output); + } + + return result.ExitCode; } - private static int PrintErrorAndReturn(string message) + private static string GetUsageText() { - Console.WriteLine(message); - return 1; + return """ + FancyZones CLI - Command line interface for FancyZones + ====================================================== + + Usage: FancyZonesCLI.exe [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) [options] + Set layout by UUID + --monitor 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) Assign hotkey (0-9) to CUSTOM layout + Note: Only custom layouts work with hotkeys + remove-hotkey (rhk) 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} + """; } - 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() + private static (int ExitCode, string Output) 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)) + if (!FancyZonesData.EditorParametersExist()) { - Console.WriteLine("Error: editor-parameters.json not found."); - Console.WriteLine("Please launch FancyZones Editor using Win+` (Win+Backtick) hotkey first."); - return 1; + return (1, "Error: editor-parameters.json not found.\nPlease launch FancyZones Editor using Win+` (Win+Backtick) hotkey first."); } // Check if editor is already running @@ -124,8 +112,7 @@ internal sealed class Program if (existingProcess != null) { NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle); - Console.WriteLine("FancyZones Editor is already running. Brought window to foreground."); - return 0; + return (0, "FancyZones Editor is already running. Brought window to foreground."); } // Only check same directory as CLI @@ -140,21 +127,18 @@ internal sealed class Program FileName = editorPath, UseShellExecute = true, }); - Console.WriteLine("FancyZones Editor launched successfully."); - return 0; + return (0, "FancyZones Editor launched successfully."); } catch (Exception ex) { - Console.WriteLine($"Failed to launch: {ex.Message}"); - return 1; + return (1, $"Failed to launch: {ex.Message}"); } } - Console.WriteLine($"Error: Could not find {editorExe} in {AppContext.BaseDirectory}"); - return 1; + return (1, $"Error: Could not find {editorExe} in {AppContext.BaseDirectory}"); } - private static int OpenSettings() + private static (int ExitCode, string Output) OpenSettings() { try { @@ -170,8 +154,7 @@ internal sealed class Program if (powertoysExe == null) { - Console.WriteLine("Error: PowerToys.exe not found. Please ensure PowerToys is installed."); - return 1; + return (1, "Error: PowerToys.exe not found. Please ensure PowerToys is installed."); } Process.Start(new ProcessStartInfo @@ -180,232 +163,180 @@ internal sealed class Program Arguments = "--open-settings=FancyZones", UseShellExecute = false, }); - Console.WriteLine("FancyZones Settings opened successfully."); - return 0; + return (0, "FancyZones Settings opened successfully."); } catch (Exception ex) { - Console.WriteLine($"Error: Failed to open FancyZones Settings. {ex.Message}"); - return 1; + return (1, $"Error: Failed to open FancyZones Settings. {ex.Message}"); } } - private static int GetMonitors() + private static (int ExitCode, string Output) GetMonitors() { - var dataPath = GetFancyZonesDataPath(); - var appliedLayoutsPath = Path.Combine(dataPath, "applied-layouts.json"); - - if (!File.Exists(appliedLayoutsPath)) + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) { - Console.WriteLine("Error: applied-layouts.json not found."); - return 1; + return (1, $"Error: {error}"); } - try + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) { - 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; + return (0, "No monitors found."); } - catch (Exception ex) + + 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++) { - Console.WriteLine($"Error: Failed to read monitor information. {ex.Message}"); - return 1; + 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()); } - private static int GetLayouts() + private static (int ExitCode, string Output) GetLayouts() { - var dataPath = GetFancyZonesDataPath(); - var templatesPath = Path.Combine(dataPath, "layout-templates.json"); - var customLayoutsPath = Path.Combine(dataPath, "custom-layouts.json"); + var sb = new System.Text.StringBuilder(); // Print template layouts - if (File.Exists(templatesPath)) + var templatesJson = FancyZonesData.ReadLayoutTemplates(); + if (templatesJson?.Templates != null) { - try + sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n"); + + for (int i = 0; i < templatesJson.Templates.Count; i++) { - var templatesJson = JsonSerializer.Deserialize(File.ReadAllText(templatesPath), FancyZonesJsonContext.Default.LayoutTemplates); - if (templatesJson?.Templates != null) + 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) { - Console.WriteLine($"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n"); + sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px"); + } - 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"); - } + sb.AppendLine(); + sb.AppendLine(); - Console.WriteLine(); + // Draw visual preview + sb.Append(LayoutVisualizer.DrawTemplateLayout(template)); - // Draw visual preview - LayoutVisualizer.DrawTemplateLayout(template); - - if (i < templatesJson.Templates.Count - 1) - { - Console.WriteLine(); - } - } - - Console.WriteLine("\n"); + if (i < templatesJson.Templates.Count - 1) + { + sb.AppendLine(); } } - catch (Exception ex) - { - Console.WriteLine($"Error parsing templates: {ex.Message}"); - } + + sb.AppendLine("\n"); } // Print custom layouts - if (File.Exists(customLayoutsPath)) + var customLayouts = FancyZonesData.ReadCustomLayouts(); + if (customLayouts?.Layouts != null) { - try - { - var customLayouts = JsonSerializer.Deserialize(File.ReadAllText(customLayoutsPath), FancyZonesJsonContext.Default.CustomLayouts); - if (customLayouts?.Layouts != null) - { - Console.WriteLine($"=== Custom Layouts ({customLayouts.Layouts.Count} total) ==="); + sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.Layouts.Count} total) ==="); - for (int i = 0; i < customLayouts.Layouts.Count; i++) + 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)) { - 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(); - } + 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; + } + } - Console.WriteLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); + 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(); } } - catch (Exception ex) - { - Console.WriteLine($"Error parsing custom layouts: {ex.Message}"); - } + + sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); } - return 0; + return (0, sb.ToString().TrimEnd()); } - private static int GetActiveLayout() + private static (int ExitCode, string Output) GetActiveLayout() { - var dataPath = GetFancyZonesDataPath(); - var appliedLayoutsPath = Path.Combine(dataPath, "applied-layouts.json"); - - if (!File.Exists(appliedLayoutsPath)) + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) { - Console.WriteLine($"Error: Could not find applied-layouts.json"); - return 1; + return (1, $"Error: {error}"); } - try + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) { - var appliedLayouts = JsonSerializer.Deserialize(File.ReadAllText(appliedLayoutsPath), FancyZonesJsonContext.Default.AppliedLayouts); - 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) { - Console.WriteLine("No active layouts found."); - return 0; + sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.AppliedLayout.Spacing}px"); } - Console.WriteLine("\n=== Active FancyZones Layout(s) ===\n"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); - for (int i = 0; i < appliedLayouts.Layouts.Count; i++) + if (i < appliedLayouts.Layouts.Count - 1) { - 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(); - } + sb.AppendLine(); } + } - return 0; - } - catch (Exception ex) - { - Console.WriteLine($"Error: {ex.Message}"); - return 1; - } + return (0, sb.ToString().TrimEnd()); } - private static int SetLayout(string[] args) + private static (int ExitCode, string Output) SetLayout(string[] args) { if (args.Length == 0) { - Console.WriteLine("Error: set-layout requires a UUID parameter"); - return 1; + return (1, "Error: set-layout requires a UUID parameter"); } string uuid = args[0]; @@ -424,8 +355,7 @@ internal sealed class Program } else { - Console.WriteLine($"Error: Invalid monitor number: {args[i + 1]}"); - return 1; + return (1, $"Error: Invalid monitor number: {args[i + 1]}"); } } else if (args[i] == "--all") @@ -436,299 +366,182 @@ internal sealed class Program if (targetMonitor.HasValue && applyToAll) { - Console.WriteLine("Error: Cannot specify both --monitor and --all"); - return 1; + return (1, "Error: Cannot specify both --monitor and --all"); } - 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"); + // 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 (!File.Exists(appliedLayoutsPath)) + // If not found in custom layouts, try template layouts (by type name) + TemplateLayout targetTemplate = null; + if (targetCustomLayout == null) { - Console.WriteLine("Error: applied-layouts.json not found"); - return 1; + var templates = FancyZonesData.ReadLayoutTemplates(); + targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase)); } - try + if (targetCustomLayout == null && targetTemplate == null) { - // 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; + 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'"); } - catch (Exception ex) + + // Read current applied layouts + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) { - Console.WriteLine($"Error: {ex.Message}"); - return 1; + return (1, $"Error: {error}"); + } + + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + return (1, "Error: No monitors configured"); + } + + // 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) + { + 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); + + // Notify FancyZones to reload + NotifyFancyZones(wmPrivAppliedLayoutsFileUpdate); + + 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"); } } - private static int GetHotkeys() + private static (int ExitCode, string Output) GetHotkeys() { - var dataPath = GetFancyZonesDataPath(); - var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); - - if (!File.Exists(hotkeysPath)) + var hotkeys = FancyZonesData.ReadLayoutHotkeys(); + if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0) { - Console.WriteLine("No hotkeys configured."); - return 0; + return (0, "No hotkeys configured."); } - try + var sb = new System.Text.StringBuilder(); + sb.AppendLine("=== Layout Hotkeys ===\n"); + sb.AppendLine("Press Win + Ctrl + Alt + to switch layouts:\n"); + + foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key)) { - 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; + sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] → {hotkey.LayoutId}"); } + + return (0, sb.ToString().TrimEnd()); } - private static int SetHotkey(int key, string layoutUuid) + private static (int ExitCode, string Output) SetHotkey(int key, string layoutUuid) { if (key < 0 || key > 9) { - Console.WriteLine("Error: Key must be between 0 and 9"); - return 1; + return (1, "Error: Key must be between 0 and 9"); } // 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; + 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; - if (File.Exists(customLayoutsPath)) + var hotkeys = FancyZonesData.ReadLayoutHotkeys() ?? 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(FancyZonesPaths.LayoutHotkeys, JsonSerializer.Serialize(hotkeys, FancyZonesJsonContext.Default.LayoutHotkeys)); + + // Notify FancyZones + NotifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + if (isCustomLayout) { - 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 - } + return (0, $"✓ Hotkey {key} assigned to custom layout '{layoutName}'\n Press Win + Ctrl + Alt + {key} to switch to this layout"); } - - var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); - - try + else { - 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; + 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."); } } - private static int RemoveHotkey(int key) + private static (int ExitCode, string Output) RemoveHotkey(int key) { - var dataPath = GetFancyZonesDataPath(); - var hotkeysPath = Path.Combine(dataPath, "layout-hotkeys.json"); - - if (!File.Exists(hotkeysPath)) + var hotkeys = FancyZonesData.ReadLayoutHotkeys(); + if (hotkeys?.Hotkeys == null) { - Console.WriteLine($"No hotkey assigned to key {key}"); - return 0; + return (0, $"No hotkey assigned to key {key}"); } - try + var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key); + if (removed == 0) { - 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; + return (0, $"No hotkey assigned to key {key}"); } - 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"); + // Save + FancyZonesData.WriteLayoutHotkeys(hotkeys); + + // Notify FancyZones + NotifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + return (0, $"Hotkey {key} removed"); } // Windows Messages for notifying FancyZones