From 8da2b024135fd95f6d4d811f6db5253c27f42e01 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Fri, 5 Dec 2025 13:35:50 +0800 Subject: [PATCH] update file structure --- .../FancyZonesCLI/Commands/EditorCommands.cs | 90 ++++ .../FancyZonesCLI/Commands/HotkeyCommands.cs | 98 ++++ .../FancyZonesCLI/Commands/LayoutCommands.cs | 272 ++++++++++ .../FancyZonesCLI/Commands/MonitorCommands.cs | 49 ++ .../fancyzones/FancyZonesCLI/NativeMethods.cs | 58 ++ .../fancyzones/FancyZonesCLI/Program.cs | 510 +----------------- 6 files changed, 584 insertions(+), 493 deletions(-) create mode 100644 src/modules/fancyzones/FancyZonesCLI/Commands/EditorCommands.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Commands/HotkeyCommands.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Commands/LayoutCommands.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Commands/MonitorCommands.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/NativeMethods.cs diff --git a/src/modules/fancyzones/FancyZonesCLI/Commands/EditorCommands.cs b/src/modules/fancyzones/FancyZonesCLI/Commands/EditorCommands.cs new file mode 100644 index 0000000000..7bf15dda44 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Commands/EditorCommands.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace FancyZonesCLI.Commands; + +/// +/// Editor and Settings commands. +/// +internal static class EditorCommands +{ + public static (int ExitCode, string Output) OpenEditor() + { + var editorExe = "PowerToys.FancyZonesEditor.exe"; + + // Check if editor-parameters.json exists + if (!FancyZonesData.EditorParametersExist()) + { + return (1, "Error: editor-parameters.json not found.\nPlease launch FancyZones Editor using Win+` (Win+Backtick) hotkey first."); + } + + // Check if editor is already running + var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault(); + if (existingProcess != null) + { + NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle); + return (0, "FancyZones Editor is already running. Brought window to foreground."); + } + + // Only check same directory as CLI + var editorPath = Path.Combine(AppContext.BaseDirectory, editorExe); + + if (File.Exists(editorPath)) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = editorPath, + UseShellExecute = true, + }); + return (0, "FancyZones Editor launched successfully."); + } + catch (Exception ex) + { + return (1, $"Failed to launch: {ex.Message}"); + } + } + + return (1, $"Error: Could not find {editorExe} in {AppContext.BaseDirectory}"); + } + + public static (int ExitCode, string Output) OpenSettings() + { + try + { + // Find PowerToys.exe in common locations + string powertoysExe = null; + + // Check in the same directory as the CLI (typical for dev builds) + var sameDirPath = Path.Combine(AppContext.BaseDirectory, "PowerToys.exe"); + if (File.Exists(sameDirPath)) + { + powertoysExe = sameDirPath; + } + + if (powertoysExe == null) + { + return (1, "Error: PowerToys.exe not found. Please ensure PowerToys is installed."); + } + + Process.Start(new ProcessStartInfo + { + FileName = powertoysExe, + Arguments = "--open-settings=FancyZones", + UseShellExecute = false, + }); + return (0, "FancyZones Settings opened successfully."); + } + catch (Exception ex) + { + return (1, $"Error: Failed to open FancyZones Settings. {ex.Message}"); + } + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Commands/HotkeyCommands.cs b/src/modules/fancyzones/FancyZonesCLI/Commands/HotkeyCommands.cs new file mode 100644 index 0000000000..cfaf93a5d4 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Commands/HotkeyCommands.cs @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace FancyZonesCLI.Commands; + +/// +/// Hotkey-related commands. +/// +internal static class HotkeyCommands +{ + public static (int ExitCode, string Output) GetHotkeys() + { + var hotkeys = FancyZonesData.ReadLayoutHotkeys(); + if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0) + { + return (0, "No hotkeys configured."); + } + + var sb = new System.Text.StringBuilder(); + sb.AppendLine("=== Layout Hotkeys ===\n"); + sb.AppendLine("Press Win + Ctrl + Alt + to switch layouts:\n"); + + foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key)) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] => {hotkey.LayoutId}"); + } + + return (0, sb.ToString().TrimEnd()); + } + + public static (int ExitCode, string Output) SetHotkey(int key, string layoutUuid, Action notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate) + { + if (key < 0 || key > 9) + { + return (1, "Error: Key must be between 0 and 9"); + } + + // Check if this is a custom layout UUID + var customLayouts = FancyZonesData.ReadCustomLayouts(); + var matchedLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(layoutUuid, StringComparison.OrdinalIgnoreCase)); + bool isCustomLayout = matchedLayout != null; + string layoutName = matchedLayout?.Name ?? layoutUuid; + + var hotkeys = FancyZonesData.ReadLayoutHotkeys() ?? new LayoutHotkeys(); + + hotkeys.Hotkeys ??= new List(); + + // Remove existing hotkey for this key + hotkeys.Hotkeys.RemoveAll(h => h.Key == key); + + // Add new hotkey + hotkeys.Hotkeys.Add(new LayoutHotkey { Key = key, LayoutId = layoutUuid }); + + // Save + FancyZonesData.WriteLayoutHotkeys(hotkeys); + + // Notify FancyZones + notifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + if (isCustomLayout) + { + return (0, $"✓ Hotkey {key} assigned to custom layout '{layoutName}'\n Press Win + Ctrl + Alt + {key} to switch to this layout"); + } + else + { + return (0, $"⚠ Warning: Hotkey {key} assigned to '{layoutUuid}'\n Note: FancyZones hotkeys only work with CUSTOM layouts.\n Template layouts (focus, columns, rows, etc.) cannot be used with hotkeys.\n Create a custom layout in the FancyZones Editor to use this hotkey."); + } + } + + public static (int ExitCode, string Output) RemoveHotkey(int key, Action notifyFancyZones, uint wmPrivLayoutHotkeysFileUpdate) + { + var hotkeys = FancyZonesData.ReadLayoutHotkeys(); + if (hotkeys?.Hotkeys == null) + { + return (0, $"No hotkey assigned to key {key}"); + } + + var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key); + if (removed == 0) + { + return (0, $"No hotkey assigned to key {key}"); + } + + // Save + FancyZonesData.WriteLayoutHotkeys(hotkeys); + + // Notify FancyZones + notifyFancyZones(wmPrivLayoutHotkeysFileUpdate); + + return (0, $"Hotkey {key} removed"); + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Commands/LayoutCommands.cs b/src/modules/fancyzones/FancyZonesCLI/Commands/LayoutCommands.cs new file mode 100644 index 0000000000..70a1c31346 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Commands/LayoutCommands.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; + +namespace FancyZonesCLI.Commands; + +/// +/// Layout-related commands. +/// +internal static class LayoutCommands +{ + public static (int ExitCode, string Output) GetLayouts() + { + var sb = new System.Text.StringBuilder(); + + // Print template layouts + var templatesJson = FancyZonesData.ReadLayoutTemplates(); + if (templatesJson?.Templates != null) + { + sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n"); + + for (int i = 0; i < templatesJson.Templates.Count; i++) + { + var template = templatesJson.Templates[i]; + sb.AppendLine(CultureInfo.InvariantCulture, $"[T{i + 1}] {template.Type}"); + sb.Append(CultureInfo.InvariantCulture, $" Zones: {template.ZoneCount}"); + if (template.ShowSpacing && template.Spacing > 0) + { + sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px"); + } + + sb.AppendLine(); + sb.AppendLine(); + + // Draw visual preview + sb.Append(LayoutVisualizer.DrawTemplateLayout(template)); + + if (i < templatesJson.Templates.Count - 1) + { + sb.AppendLine(); + } + } + + sb.AppendLine("\n"); + } + + // Print custom layouts + var customLayouts = FancyZonesData.ReadCustomLayouts(); + if (customLayouts?.Layouts != null) + { + sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.Layouts.Count} total) ==="); + + for (int i = 0; i < customLayouts.Layouts.Count; i++) + { + var layout = customLayouts.Layouts[i]; + sb.AppendLine(CultureInfo.InvariantCulture, $"[{i + 1}] {layout.Name}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.Uuid}"); + sb.Append(CultureInfo.InvariantCulture, $" Type: {layout.Type}"); + + bool isCanvasLayout = false; + if (layout.Info.ValueKind != JsonValueKind.Undefined && layout.Info.ValueKind != JsonValueKind.Null) + { + if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && layout.Info.TryGetProperty("columns", out var cols)) + { + sb.Append(CultureInfo.InvariantCulture, $" ({rows.GetInt32()}x{cols.GetInt32()} grid)"); + } + else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones)) + { + sb.Append(CultureInfo.InvariantCulture, $" ({zones.GetArrayLength()} zones)"); + isCanvasLayout = true; + } + } + + sb.AppendLine("\n"); + + // Draw visual preview + sb.Append(LayoutVisualizer.DrawCustomLayout(layout)); + + // Add note for canvas layouts + if (isCanvasLayout) + { + sb.AppendLine("\n Note: Canvas layout preview is approximate."); + sb.AppendLine(" Open FancyZones Editor for precise zone boundaries."); + } + + if (i < customLayouts.Layouts.Count - 1) + { + sb.AppendLine(); + } + } + + sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); + } + + return (0, sb.ToString().TrimEnd()); + } + + public static (int ExitCode, string Output) GetActiveLayout() + { + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) + { + return (1, $"Error: {error}"); + } + + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + return (0, "No active layouts found."); + } + + var sb = new System.Text.StringBuilder(); + sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n"); + + for (int i = 0; i < appliedLayouts.Layouts.Count; i++) + { + var layout = appliedLayouts.Layouts[i]; + sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {i + 1}:"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Name: {layout.AppliedLayout.Type}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.AppliedLayout.Uuid}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Type: {layout.AppliedLayout.Type} ({layout.AppliedLayout.ZoneCount} zones)"); + + if (layout.AppliedLayout.ShowSpacing) + { + sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.AppliedLayout.Spacing}px"); + } + + sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); + + if (i < appliedLayouts.Layouts.Count - 1) + { + sb.AppendLine(); + } + } + + return (0, sb.ToString().TrimEnd()); + } + + public static (int ExitCode, string Output) SetLayout(string[] args, Action notifyFancyZones, uint wmPrivAppliedLayoutsFileUpdate) + { + if (args.Length == 0) + { + return (1, "Error: set-layout requires a UUID parameter"); + } + + string uuid = args[0]; + int? targetMonitor = null; + bool applyToAll = false; + + // Parse options + for (int i = 1; i < args.Length; i++) + { + if (args[i] == "--monitor" && i + 1 < args.Length) + { + if (int.TryParse(args[i + 1], out int monitorNum)) + { + targetMonitor = monitorNum; + i++; // Skip next arg + } + else + { + return (1, $"Error: Invalid monitor number: {args[i + 1]}"); + } + } + else if (args[i] == "--all") + { + applyToAll = true; + } + } + + if (targetMonitor.HasValue && applyToAll) + { + return (1, "Error: Cannot specify both --monitor and --all"); + } + + // Try to find layout in custom layouts first (by UUID) + var customLayouts = FancyZonesData.ReadCustomLayouts(); + var targetCustomLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(uuid, StringComparison.OrdinalIgnoreCase)); + + // If not found in custom layouts, try template layouts (by type name) + TemplateLayout targetTemplate = null; + if (targetCustomLayout == null) + { + var templates = FancyZonesData.ReadLayoutTemplates(); + targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase)); + } + + if (targetCustomLayout == null && targetTemplate == null) + { + return (1, $"Error: Layout '{uuid}' not found\nTip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')\n For custom layouts, use the UUID from 'get-layouts'"); + } + + // Read current applied layouts + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) + { + return (1, $"Error: {error}"); + } + + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + return (1, "Error: No monitors configured"); + } + + // Determine which monitors to update + List 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"); + } + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Commands/MonitorCommands.cs b/src/modules/fancyzones/FancyZonesCLI/Commands/MonitorCommands.cs new file mode 100644 index 0000000000..f542b901cc --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Commands/MonitorCommands.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; + +namespace FancyZonesCLI.Commands; + +/// +/// Monitor-related commands. +/// +internal static class MonitorCommands +{ + public static (int ExitCode, string Output) GetMonitors() + { + if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) + { + return (1, $"Error: {error}"); + } + + if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) + { + return (0, "No monitors found."); + } + + var sb = new System.Text.StringBuilder(); + sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({appliedLayouts.Layouts.Count} total) ==="); + sb.AppendLine(); + + for (int i = 0; i < appliedLayouts.Layouts.Count; i++) + { + var layout = appliedLayouts.Layouts[i]; + var monitorNum = i + 1; + + sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {monitorNum}:"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor: {layout.Device.Monitor}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Instance: {layout.Device.MonitorInstance}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Number: {layout.Device.MonitorNumber}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Serial Number: {layout.Device.SerialNumber}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Virtual Desktop: {layout.Device.VirtualDesktop}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Active Layout: {layout.AppliedLayout.Type}"); + sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {layout.AppliedLayout.ZoneCount}"); + sb.AppendLine(); + } + + return (0, sb.ToString().TrimEnd()); + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/NativeMethods.cs b/src/modules/fancyzones/FancyZonesCLI/NativeMethods.cs new file mode 100644 index 0000000000..65adc5eccd --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/NativeMethods.cs @@ -0,0 +1,58 @@ +// 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.Runtime.InteropServices; + +namespace FancyZonesCLI; + +/// +/// Native Windows API methods for FancyZones CLI. +/// +internal static class NativeMethods +{ + public static readonly IntPtr HWND_BROADCAST = new IntPtr(0xffff); + + // Registered Windows messages for notifying FancyZones + private static uint wmPrivAppliedLayoutsFileUpdate; + private static uint wmPrivLayoutHotkeysFileUpdate; + + /// + /// Gets the Windows message ID for applied layouts file update notification. + /// + public static uint WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE => wmPrivAppliedLayoutsFileUpdate; + + /// + /// Gets the Windows message ID for layout hotkeys file update notification. + /// + public static uint WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE => wmPrivLayoutHotkeysFileUpdate; + + /// + /// Initializes the Windows messages used for FancyZones notifications. + /// + public static void InitializeWindowMessages() + { + wmPrivAppliedLayoutsFileUpdate = RegisterWindowMessage("{2ef2c8a7-e0d5-4f31-9ede-52aade2d284d}"); + wmPrivLayoutHotkeysFileUpdate = RegisterWindowMessage("{07229b7e-4f22-4357-b136-33c289be2295}"); + } + + /// + /// Broadcasts a notification message to FancyZones. + /// + /// The Windows message ID to broadcast. + public static void NotifyFancyZones(uint message) + { + PostMessage(HWND_BROADCAST, message, IntPtr.Zero, IntPtr.Zero); + } + + [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); +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Program.cs b/src/modules/fancyzones/FancyZonesCLI/Program.cs index d8781392f3..698f8f0e51 100644 --- a/src/modules/fancyzones/FancyZonesCLI/Program.cs +++ b/src/modules/fancyzones/FancyZonesCLI/Program.cs @@ -3,13 +3,9 @@ // 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; +using FancyZonesCLI.Commands; namespace FancyZonesCLI; @@ -18,7 +14,7 @@ internal sealed class Program private static int Main(string[] args) { // Initialize Windows messages - InitializeWindowMessages(); + NativeMethods.InitializeWindowMessages(); (int ExitCode, string Output) result; @@ -32,15 +28,21 @@ internal sealed class Program 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 "), + "open-editor" or "editor" or "e" => EditorCommands.OpenEditor(), + "get-monitors" or "monitors" or "m" => MonitorCommands.GetMonitors(), + "get-layouts" or "layouts" or "ls" => LayoutCommands.GetLayouts(), + "get-active-layout" or "active" or "get-active" or "a" => LayoutCommands.GetActiveLayout(), + "set-layout" or "set" or "s" => args.Length >= 2 + ? LayoutCommands.SetLayout(args.Skip(1).ToArray(), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_APPLIED_LAYOUTS_FILE_UPDATE) + : (1, "Error: set-layout requires a UUID parameter"), + "open-settings" or "settings" => EditorCommands.OpenSettings(), + "get-hotkeys" or "hotkeys" or "hk" => HotkeyCommands.GetHotkeys(), + "set-hotkey" or "shk" => args.Length >= 3 + ? HotkeyCommands.SetHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), args[2], NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE) + : (1, "Error: set-hotkey requires "), + "remove-hotkey" or "rhk" => args.Length >= 2 + ? HotkeyCommands.RemoveHotkey(int.Parse(args[1], CultureInfo.InvariantCulture), NativeMethods.NotifyFancyZones, NativeMethods.WM_PRIV_LAYOUT_HOTKEYS_FILE_UPDATE) + : (1, "Error: remove-hotkey requires "), "help" or "--help" or "-h" => (0, GetUsageText()), _ => (1, $"Error: Unknown command: {command}\n\n{GetUsageText()}"), }; @@ -96,482 +98,4 @@ internal sealed class Program FancyZonesCLI.exe set-hotkey 3 {12345678-1234-1234-1234-123456789012} """; } - - private static (int ExitCode, string Output) OpenEditor() - { - var editorExe = "PowerToys.FancyZonesEditor.exe"; - - // Check if editor-parameters.json exists - if (!FancyZonesData.EditorParametersExist()) - { - return (1, "Error: editor-parameters.json not found.\nPlease launch FancyZones Editor using Win+` (Win+Backtick) hotkey first."); - } - - // Check if editor is already running - var existingProcess = Process.GetProcessesByName("PowerToys.FancyZonesEditor").FirstOrDefault(); - if (existingProcess != null) - { - NativeMethods.SetForegroundWindow(existingProcess.MainWindowHandle); - return (0, "FancyZones Editor is already running. Brought window to foreground."); - } - - // Only check same directory as CLI - var editorPath = Path.Combine(AppContext.BaseDirectory, editorExe); - - if (File.Exists(editorPath)) - { - try - { - Process.Start(new ProcessStartInfo - { - FileName = editorPath, - UseShellExecute = true, - }); - return (0, "FancyZones Editor launched successfully."); - } - catch (Exception ex) - { - return (1, $"Failed to launch: {ex.Message}"); - } - } - - return (1, $"Error: Could not find {editorExe} in {AppContext.BaseDirectory}"); - } - - private static (int ExitCode, string Output) OpenSettings() - { - try - { - // Find PowerToys.exe in common locations - string powertoysExe = null; - - // Check in the same directory as the CLI (typical for dev builds) - var sameDirPath = Path.Combine(AppContext.BaseDirectory, "PowerToys.exe"); - if (File.Exists(sameDirPath)) - { - powertoysExe = sameDirPath; - } - - if (powertoysExe == null) - { - return (1, "Error: PowerToys.exe not found. Please ensure PowerToys is installed."); - } - - Process.Start(new ProcessStartInfo - { - FileName = powertoysExe, - Arguments = "--open-settings=FancyZones", - UseShellExecute = false, - }); - return (0, "FancyZones Settings opened successfully."); - } - catch (Exception ex) - { - return (1, $"Error: Failed to open FancyZones Settings. {ex.Message}"); - } - } - - private static (int ExitCode, string Output) GetMonitors() - { - if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) - { - return (1, $"Error: {error}"); - } - - if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) - { - return (0, "No monitors found."); - } - - var sb = new System.Text.StringBuilder(); - sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({appliedLayouts.Layouts.Count} total) ==="); - sb.AppendLine(); - - for (int i = 0; i < appliedLayouts.Layouts.Count; i++) - { - var layout = appliedLayouts.Layouts[i]; - var monitorNum = i + 1; - - sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {monitorNum}:"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor: {layout.Device.Monitor}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Instance: {layout.Device.MonitorInstance}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Monitor Number: {layout.Device.MonitorNumber}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Serial Number: {layout.Device.SerialNumber}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Virtual Desktop: {layout.Device.VirtualDesktop}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Active Layout: {layout.AppliedLayout.Type}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Zone Count: {layout.AppliedLayout.ZoneCount}"); - sb.AppendLine(); - } - - return (0, sb.ToString().TrimEnd()); - } - - private static (int ExitCode, string Output) GetLayouts() - { - var sb = new System.Text.StringBuilder(); - - // Print template layouts - var templatesJson = FancyZonesData.ReadLayoutTemplates(); - if (templatesJson?.Templates != null) - { - sb.AppendLine(CultureInfo.InvariantCulture, $"=== Built-in Template Layouts ({templatesJson.Templates.Count} total) ===\n"); - - for (int i = 0; i < templatesJson.Templates.Count; i++) - { - var template = templatesJson.Templates[i]; - sb.AppendLine(CultureInfo.InvariantCulture, $"[T{i + 1}] {template.Type}"); - sb.Append(CultureInfo.InvariantCulture, $" Zones: {template.ZoneCount}"); - if (template.ShowSpacing && template.Spacing > 0) - { - sb.Append(CultureInfo.InvariantCulture, $", Spacing: {template.Spacing}px"); - } - - sb.AppendLine(); - sb.AppendLine(); - - // Draw visual preview - sb.Append(LayoutVisualizer.DrawTemplateLayout(template)); - - if (i < templatesJson.Templates.Count - 1) - { - sb.AppendLine(); - } - } - - sb.AppendLine("\n"); - } - - // Print custom layouts - var customLayouts = FancyZonesData.ReadCustomLayouts(); - if (customLayouts?.Layouts != null) - { - sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.Layouts.Count} total) ==="); - - for (int i = 0; i < customLayouts.Layouts.Count; i++) - { - var layout = customLayouts.Layouts[i]; - sb.AppendLine(CultureInfo.InvariantCulture, $"[{i + 1}] {layout.Name}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.Uuid}"); - sb.Append(CultureInfo.InvariantCulture, $" Type: {layout.Type}"); - - bool isCanvasLayout = false; - if (layout.Info.ValueKind != JsonValueKind.Undefined && layout.Info.ValueKind != JsonValueKind.Null) - { - if (layout.Type == "grid" && layout.Info.TryGetProperty("rows", out var rows) && layout.Info.TryGetProperty("columns", out var cols)) - { - sb.Append(CultureInfo.InvariantCulture, $" ({rows.GetInt32()}x{cols.GetInt32()} grid)"); - } - else if (layout.Type == "canvas" && layout.Info.TryGetProperty("zones", out var zones)) - { - sb.Append(CultureInfo.InvariantCulture, $" ({zones.GetArrayLength()} zones)"); - isCanvasLayout = true; - } - } - - sb.AppendLine("\n"); - - // Draw visual preview - sb.Append(LayoutVisualizer.DrawCustomLayout(layout)); - - // Add note for canvas layouts - if (isCanvasLayout) - { - sb.AppendLine("\n Note: Canvas layout preview is approximate."); - sb.AppendLine(" Open FancyZones Editor for precise zone boundaries."); - } - - if (i < customLayouts.Layouts.Count - 1) - { - sb.AppendLine(); - } - } - - sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); - } - - return (0, sb.ToString().TrimEnd()); - } - - private static (int ExitCode, string Output) GetActiveLayout() - { - if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) - { - return (1, $"Error: {error}"); - } - - if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) - { - return (0, "No active layouts found."); - } - - var sb = new System.Text.StringBuilder(); - sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n"); - - for (int i = 0; i < appliedLayouts.Layouts.Count; i++) - { - var layout = appliedLayouts.Layouts[i]; - sb.AppendLine(CultureInfo.InvariantCulture, $"Monitor {i + 1}:"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Name: {layout.AppliedLayout.Type}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" UUID: {layout.AppliedLayout.Uuid}"); - sb.AppendLine(CultureInfo.InvariantCulture, $" Type: {layout.AppliedLayout.Type} ({layout.AppliedLayout.ZoneCount} zones)"); - - if (layout.AppliedLayout.ShowSpacing) - { - sb.AppendLine(CultureInfo.InvariantCulture, $" Spacing: {layout.AppliedLayout.Spacing}px"); - } - - sb.AppendLine(CultureInfo.InvariantCulture, $" Sensitivity Radius: {layout.AppliedLayout.SensitivityRadius}px"); - - if (i < appliedLayouts.Layouts.Count - 1) - { - sb.AppendLine(); - } - } - - return (0, sb.ToString().TrimEnd()); - } - - private static (int ExitCode, string Output) SetLayout(string[] args) - { - if (args.Length == 0) - { - return (1, "Error: set-layout requires a UUID parameter"); - } - - string uuid = args[0]; - int? targetMonitor = null; - bool applyToAll = false; - - // Parse options - for (int i = 1; i < args.Length; i++) - { - if (args[i] == "--monitor" && i + 1 < args.Length) - { - if (int.TryParse(args[i + 1], out int monitorNum)) - { - targetMonitor = monitorNum; - i++; // Skip next arg - } - else - { - return (1, $"Error: Invalid monitor number: {args[i + 1]}"); - } - } - else if (args[i] == "--all") - { - applyToAll = true; - } - } - - if (targetMonitor.HasValue && applyToAll) - { - return (1, "Error: Cannot specify both --monitor and --all"); - } - - // Try to find layout in custom layouts first (by UUID) - var customLayouts = FancyZonesData.ReadCustomLayouts(); - var targetCustomLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(uuid, StringComparison.OrdinalIgnoreCase)); - - // If not found in custom layouts, try template layouts (by type name) - TemplateLayout targetTemplate = null; - if (targetCustomLayout == null) - { - var templates = FancyZonesData.ReadLayoutTemplates(); - targetTemplate = templates?.Templates?.FirstOrDefault(t => t.Type.Equals(uuid, StringComparison.OrdinalIgnoreCase)); - } - - if (targetCustomLayout == null && targetTemplate == null) - { - return (1, $"Error: Layout '{uuid}' not found\nTip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')\n For custom layouts, use the UUID from 'get-layouts'"); - } - - // Read current applied layouts - if (!FancyZonesData.TryReadAppliedLayouts(out var appliedLayouts, out var error)) - { - return (1, $"Error: {error}"); - } - - if (appliedLayouts.Layouts == null || appliedLayouts.Layouts.Count == 0) - { - return (1, "Error: No monitors configured"); - } - - // Determine which monitors to update - List 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 ExitCode, string Output) GetHotkeys() - { - var hotkeys = FancyZonesData.ReadLayoutHotkeys(); - if (hotkeys?.Hotkeys == null || hotkeys.Hotkeys.Count == 0) - { - return (0, "No hotkeys configured."); - } - - var sb = new System.Text.StringBuilder(); - sb.AppendLine("=== Layout Hotkeys ===\n"); - sb.AppendLine("Press Win + Ctrl + Alt + to switch layouts:\n"); - - foreach (var hotkey in hotkeys.Hotkeys.OrderBy(h => h.Key)) - { - sb.AppendLine(CultureInfo.InvariantCulture, $" [{hotkey.Key}] → {hotkey.LayoutId}"); - } - - return (0, sb.ToString().TrimEnd()); - } - - private static (int ExitCode, string Output) SetHotkey(int key, string layoutUuid) - { - if (key < 0 || key > 9) - { - return (1, "Error: Key must be between 0 and 9"); - } - - // Check if this is a custom layout UUID - var customLayouts = FancyZonesData.ReadCustomLayouts(); - var matchedLayout = customLayouts?.Layouts?.FirstOrDefault(l => l.Uuid.Equals(layoutUuid, StringComparison.OrdinalIgnoreCase)); - bool isCustomLayout = matchedLayout != null; - string layoutName = matchedLayout?.Name ?? layoutUuid; - - var hotkeys = FancyZonesData.ReadLayoutHotkeys() ?? new LayoutHotkeys(); - - hotkeys.Hotkeys ??= new List(); - - // 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) - { - return (0, $"✓ Hotkey {key} assigned to custom layout '{layoutName}'\n Press Win + Ctrl + Alt + {key} to switch to this layout"); - } - else - { - return (0, $"⚠ Warning: Hotkey {key} assigned to '{layoutUuid}'\n Note: FancyZones hotkeys only work with CUSTOM layouts.\n Template layouts (focus, columns, rows, etc.) cannot be used with hotkeys.\n Create a custom layout in the FancyZones Editor to use this hotkey."); - } - } - - private static (int ExitCode, string Output) RemoveHotkey(int key) - { - var hotkeys = FancyZonesData.ReadLayoutHotkeys(); - if (hotkeys?.Hotkeys == null) - { - return (0, $"No hotkey assigned to key {key}"); - } - - var removed = hotkeys.Hotkeys.RemoveAll(h => h.Key == key); - if (removed == 0) - { - return (0, $"No hotkey assigned to key {key}"); - } - - // Save - FancyZonesData.WriteLayoutHotkeys(hotkeys); - - // Notify FancyZones - NotifyFancyZones(wmPrivLayoutHotkeysFileUpdate); - - return (0, $"Hotkey {key} removed"); - } - - // 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); }