From 3f106344b34f01962c79fd73f1daf074272a3567 Mon Sep 17 00:00:00 2001 From: leileizhang Date: Thu, 25 Dec 2025 12:23:25 +0800 Subject: [PATCH] [FancyZones CLI] Add localization and telemetry support (#44421) ## Summary of the Pull Request This PR adds comprehensive localization and telemetry support to the FancyZones CLI, improving user experience for international users and enabling usage tracking for product insights. ## PR Checklist - [ ] Closes: #xxx - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- DATA_AND_PRIVACY.md | 4 + .../Commands/FancyZonesBaseCommand.cs | 29 +- .../Commands/GetActiveLayoutCommand.cs | 10 +- .../CommandLine/Commands/GetHotkeysCommand.cs | 8 +- .../CommandLine/Commands/GetLayoutsCommand.cs | 10 +- .../Commands/GetMonitorsCommand.cs | 8 +- .../CommandLine/Commands/OpenEditorCommand.cs | 5 +- .../Commands/OpenSettingsCommand.cs | 7 +- .../Commands/RemoveHotkeyCommand.cs | 9 +- .../CommandLine/Commands/SetHotkeyCommand.cs | 11 +- .../CommandLine/Commands/SetLayoutCommand.cs | 31 +- .../FancyZonesCLI/FancyZonesCLI.csproj | 18 +- .../Properties/Resources.Designer.cs | 353 ++++++++++++++++++ .../FancyZonesCLI/Properties/Resources.resx | 233 ++++++++++++ .../Telemetry/FancyZonesCLICommandEvent.cs | 36 ++ .../Utils/EditorParametersRefresh.cs | 3 +- 16 files changed, 723 insertions(+), 52 deletions(-) create mode 100644 src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs create mode 100644 src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx create mode 100644 src/modules/fancyzones/FancyZonesCLI/Telemetry/FancyZonesCLICommandEvent.cs diff --git a/DATA_AND_PRIVACY.md b/DATA_AND_PRIVACY.md index c2699e7f9d..66a0daa1d8 100644 --- a/DATA_AND_PRIVACY.md +++ b/DATA_AND_PRIVACY.md @@ -444,6 +444,10 @@ _If you want to find diagnostic data events in the source code, these two links Microsoft.PowerToys.FancyZones_ZoneWindowKeyUp Occurs when a key is released while interacting with zones. + + Microsoft.PowerToys.FancyZones_CLICommand + Triggered when a FancyZones CLI command is executed, logging the command name and success status. + ### FileExplorerAddOns diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/FancyZonesBaseCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/FancyZonesBaseCommand.cs index d47fc42cdf..57fd4c3dad 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/FancyZonesBaseCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/FancyZonesBaseCommand.cs @@ -8,6 +8,8 @@ using System.CommandLine.Invocation; using FancyZonesCLI; using FancyZonesCLI.CommandLine; +using FancyZonesCLI.Telemetry; +using Microsoft.PowerToys.Telemetry; namespace FancyZonesCLI.CommandLine.Commands; @@ -24,12 +26,14 @@ internal abstract class FancyZonesBaseCommand : Command private void InvokeInternal(InvocationContext context) { Logger.LogInfo($"Executing command '{Name}'"); + bool successful = false; if (!FancyZonesCliGuards.IsFancyZonesRunning()) { Logger.LogWarning($"Command '{Name}' blocked: FancyZones is not running"); - context.Console.Error.Write($"Error: FancyZones is not running. Start PowerToys (FancyZones) and retry.{Environment.NewLine}"); + context.Console.Error.Write($"{Properties.Resources.error_fancyzones_not_running}{Environment.NewLine}"); context.ExitCode = 1; + LogTelemetry(successful: false); return; } @@ -37,6 +41,7 @@ internal abstract class FancyZonesBaseCommand : Command { string output = Execute(context); context.ExitCode = 0; + successful = true; Logger.LogInfo($"Command '{Name}' completed successfully"); Logger.LogDebug($"Command '{Name}' output length: {output?.Length ?? 0}"); @@ -52,6 +57,28 @@ internal abstract class FancyZonesBaseCommand : Command Logger.LogError($"Command '{Name}' failed", ex); context.Console.Error.Write($"Error: {ex.Message}{Environment.NewLine}"); context.ExitCode = 1; + successful = false; + } + finally + { + LogTelemetry(successful); + } + } + + private void LogTelemetry(bool successful) + { + try + { + PowerToysTelemetry.Log.WriteEvent(new FancyZonesCLICommandEvent + { + CommandName = Name, + Successful = successful, + }); + } + catch (Exception ex) + { + // Don't fail the command if telemetry logging fails + Logger.LogError($"Failed to log telemetry for command '{Name}'", ex); } } } diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetActiveLayoutCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetActiveLayoutCommand.cs index d2fc0f4f9d..14e7fe71c5 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetActiveLayoutCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetActiveLayoutCommand.cs @@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand { public GetActiveLayoutCommand() - : base("get-active-layout", "Show currently active layout") + : base("get-active-layout", Properties.Resources.cmd_get_active_layout) { AddAlias("active"); } @@ -28,7 +28,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand if (editorParams.Monitors == null || editorParams.Monitors.Count == 0) { - throw new InvalidOperationException("Could not get current monitor information."); + throw new InvalidOperationException(Properties.Resources.get_active_layout_no_monitor_info); } // Read applied layouts. @@ -36,11 +36,11 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand if (appliedLayouts.AppliedLayouts == null) { - return "No layouts configured."; + return Properties.Resources.get_active_layout_no_layouts; } var sb = new System.Text.StringBuilder(); - sb.AppendLine("\n=== Active FancyZones Layout(s) ===\n"); + sb.AppendLine($"\n{Properties.Resources.get_active_layout_header}\n"); // Show only layouts for currently connected monitors. for (int i = 0; i < editorParams.Monitors.Count; i++) @@ -71,7 +71,7 @@ internal sealed partial class GetActiveLayoutCommand : FancyZonesBaseCommand } else { - sb.AppendLine(" No layout applied"); + sb.AppendLine(Properties.Resources.get_active_layout_no_layout); } if (i < editorParams.Monitors.Count - 1) diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetHotkeysCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetHotkeysCommand.cs index 342598c822..7f91849922 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetHotkeysCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetHotkeysCommand.cs @@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand { public GetHotkeysCommand() - : base("get-hotkeys", "List all layout hotkeys") + : base("get-hotkeys", Properties.Resources.cmd_get_hotkeys) { AddAlias("hk"); } @@ -26,12 +26,12 @@ internal sealed partial class GetHotkeysCommand : FancyZonesBaseCommand if (hotkeys.LayoutHotkeys == null || hotkeys.LayoutHotkeys.Count == 0) { - return "No hotkeys configured."; + return Properties.Resources.get_hotkeys_no_hotkeys; } var sb = new System.Text.StringBuilder(); - sb.AppendLine("=== Layout Hotkeys ===\n"); - sb.AppendLine("Press Win + Ctrl + Alt + to switch layouts:\n"); + sb.AppendLine($"{Properties.Resources.get_hotkeys_header}\n"); + sb.AppendLine($"{Properties.Resources.get_hotkeys_instruction}\n"); foreach (var hotkey in hotkeys.LayoutHotkeys.OrderBy(h => h.Key)) { diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetLayoutsCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetLayoutsCommand.cs index fc754fb95b..f7fbd43eb7 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetLayoutsCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetLayoutsCommand.cs @@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand { public GetLayoutsCommand() - : base("get-layouts", "List available layouts") + : base("get-layouts", Properties.Resources.cmd_get_layouts) { AddAlias("ls"); } @@ -61,7 +61,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand if (customLayouts.CustomLayouts != null) { - sb.AppendLine(CultureInfo.InvariantCulture, $"=== Custom Layouts ({customLayouts.CustomLayouts.Count} total) ==="); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_layouts_custom_header, customLayouts.CustomLayouts.Count)); for (int i = 0; i < customLayouts.CustomLayouts.Count; i++) { @@ -92,8 +92,8 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand // 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."); + sb.AppendLine($"\n {Properties.Resources.get_layouts_canvas_note}"); + sb.AppendLine($" {Properties.Resources.get_layouts_canvas_detail}"); } if (i < customLayouts.CustomLayouts.Count - 1) @@ -102,7 +102,7 @@ internal sealed partial class GetLayoutsCommand : FancyZonesBaseCommand } } - sb.AppendLine("\nUse 'FancyZonesCLI.exe set-layout ' to apply a layout."); + sb.AppendLine($"\n{Properties.Resources.get_layouts_usage}"); } return sb.ToString().TrimEnd(); diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetMonitorsCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetMonitorsCommand.cs index f8e8ebb189..700ee656ce 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetMonitorsCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/GetMonitorsCommand.cs @@ -15,7 +15,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand { public GetMonitorsCommand() - : base("get-monitors", "List monitors and FancyZones metadata") + : base("get-monitors", Properties.Resources.cmd_get_monitors) { AddAlias("m"); } @@ -31,19 +31,19 @@ internal sealed partial class GetMonitorsCommand : FancyZonesBaseCommand } catch (Exception ex) { - throw new InvalidOperationException($"Failed to read monitor information. {ex.Message}{Environment.NewLine}Note: Ensure FancyZones is running to get current monitor information.", ex); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_error, ex.Message), ex); } if (editorParams.Monitors == null || editorParams.Monitors.Count == 0) { - return "No monitors found."; + return Properties.Resources.get_monitors_no_monitors; } // Also read applied layouts to show which layout is active on each monitor. var appliedLayouts = FancyZonesDataIO.ReadAppliedLayouts(); var sb = new System.Text.StringBuilder(); - sb.AppendLine(CultureInfo.InvariantCulture, $"=== Monitors ({editorParams.Monitors.Count} total) ==="); + sb.AppendLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.get_monitors_header, editorParams.Monitors.Count)); sb.AppendLine(); for (int i = 0; i < editorParams.Monitors.Count; i++) diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenEditorCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenEditorCommand.cs index 122d3a000f..5cdb6c1557 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenEditorCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenEditorCommand.cs @@ -5,6 +5,7 @@ using System; using System.CommandLine.Invocation; using System.Diagnostics; +using System.Globalization; using System.Linq; using System.Threading; @@ -13,7 +14,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand { public OpenEditorCommand() - : base("open-editor", "Launch FancyZones layout editor") + : base("open-editor", Properties.Resources.cmd_open_editor) { AddAlias("e"); } @@ -38,7 +39,7 @@ internal sealed partial class OpenEditorCommand : FancyZonesBaseCommand } catch (Exception ex) { - throw new InvalidOperationException($"Failed to request FancyZones Editor launch. {ex.Message}", ex); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_editor_error, ex.Message), ex); } } } diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenSettingsCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenSettingsCommand.cs index 84698bb9b6..ce55d82d79 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenSettingsCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/OpenSettingsCommand.cs @@ -5,6 +5,7 @@ using System; using System.CommandLine.Invocation; using System.Diagnostics; +using System.Globalization; using System.IO; namespace FancyZonesCLI.CommandLine.Commands; @@ -12,7 +13,7 @@ namespace FancyZonesCLI.CommandLine.Commands; internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand { public OpenSettingsCommand() - : base("open-settings", "Open FancyZones settings page") + : base("open-settings", Properties.Resources.cmd_open_settings) { AddAlias("settings"); } @@ -37,14 +38,14 @@ internal sealed partial class OpenSettingsCommand : FancyZonesBaseCommand if (process == null) { - throw new InvalidOperationException("PowerToys.exe failed to start."); + throw new InvalidOperationException(Properties.Resources.open_settings_error_not_started); } return string.Empty; } catch (Exception ex) { - throw new InvalidOperationException($"Failed to open FancyZones Settings. {ex.Message}", ex); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.open_settings_error, ex.Message), ex); } } } diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/RemoveHotkeyCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/RemoveHotkeyCommand.cs index 4f88e268c5..64de0bcc8a 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/RemoveHotkeyCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/RemoveHotkeyCommand.cs @@ -5,6 +5,7 @@ using System; using System.CommandLine; using System.CommandLine.Invocation; +using System.Globalization; using FancyZonesEditorCommon.Data; using FancyZonesEditorCommon.Utils; @@ -16,11 +17,11 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand private readonly Argument _key; public RemoveHotkeyCommand() - : base("remove-hotkey", "Remove hotkey assignment") + : base("remove-hotkey", Properties.Resources.cmd_remove_hotkey) { AddAlias("rhk"); - _key = new Argument("key", "Hotkey index (0-9)"); + _key = new Argument("key", Properties.Resources.remove_hotkey_arg_key); AddArgument(_key); } @@ -33,14 +34,14 @@ internal sealed partial class RemoveHotkeyCommand : FancyZonesBaseCommand if (hotkeysWrapper.LayoutHotkeys == null) { - return "No hotkeys configured."; + return Properties.Resources.remove_hotkey_no_hotkeys; } var hotkeysList = hotkeysWrapper.LayoutHotkeys; var removed = hotkeysList.RemoveAll(h => h.Key == key); if (removed == 0) { - return $"No hotkey assigned to key {key}"; + return string.Format(CultureInfo.InvariantCulture, Properties.Resources.remove_hotkey_not_found, key); } // Save. diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetHotkeyCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetHotkeyCommand.cs index 4982be284c..993bb4b095 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetHotkeyCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetHotkeyCommand.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.CommandLine; using System.CommandLine.Invocation; +using System.Globalization; using System.Linq; using FancyZonesEditorCommon.Data; @@ -19,12 +20,12 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand private readonly Argument _layout; public SetHotkeyCommand() - : base("set-hotkey", "Assign hotkey (0-9) to a custom layout") + : base("set-hotkey", Properties.Resources.cmd_set_hotkey) { AddAlias("shk"); - _key = new Argument("key", "Hotkey index (0-9)"); - _layout = new Argument("layout", "Custom layout UUID"); + _key = new Argument("key", Properties.Resources.set_hotkey_arg_key); + _layout = new Argument("layout", Properties.Resources.set_hotkey_arg_layout); AddArgument(_key); AddArgument(_layout); @@ -38,7 +39,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand if (key < 0 || key > 9) { - throw new InvalidOperationException("Key must be between 0 and 9."); + throw new InvalidOperationException(Properties.Resources.set_hotkey_error_invalid_key); } // Editor only allows assigning hotkeys to existing custom layouts. @@ -59,7 +60,7 @@ internal sealed partial class SetHotkeyCommand : FancyZonesBaseCommand if (!matchedLayout.HasValue) { - throw new InvalidOperationException($"Layout '{layout}' is not a custom layout UUID."); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_hotkey_error_not_custom, layout)); } string layoutName = matchedLayout.Value.Name; diff --git a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetLayoutCommand.cs b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetLayoutCommand.cs index 3d2a204af9..1b3ca079b5 100644 --- a/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetLayoutCommand.cs +++ b/src/modules/fancyzones/FancyZonesCLI/CommandLine/Commands/SetLayoutCommand.cs @@ -26,14 +26,14 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand private readonly Option _all; public SetLayoutCommand() - : base("set-layout", "Set layout by UUID or template name") + : base("set-layout", Properties.Resources.cmd_set_layout) { AddAlias("s"); - _layoutId = new Argument("layout", "Layout UUID or template type (e.g. focus, columns)"); + _layoutId = new Argument("layout", Properties.Resources.set_layout_arg_layout); AddArgument(_layoutId); - _monitor = new Option(AliasesMonitor, "Apply to monitor N (1-based)"); + _monitor = new Option(AliasesMonitor, Properties.Resources.set_layout_opt_monitor); _monitor.AddValidator(result => { if (result.Tokens.Count == 0) @@ -44,11 +44,11 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand int? monitor = result.GetValueOrDefault(); if (monitor.HasValue && monitor.Value < 1) { - result.ErrorMessage = "Monitor index must be >= 1."; + result.ErrorMessage = Properties.Resources.set_layout_error_monitor_index; } }); - _all = new Option(AliasesAll, "Apply to all monitors"); + _all = new Option(AliasesAll, Properties.Resources.set_layout_opt_all); AddOption(_monitor); AddOption(_all); @@ -60,7 +60,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand if (monitor.HasValue && all) { - commandResult.ErrorMessage = "Cannot specify both --monitor and --all."; + commandResult.ErrorMessage = Properties.Resources.set_layout_error_both_options; } }); } @@ -97,15 +97,15 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand { if (all) { - return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to all monitors.", layout); + return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_all, layout); } if (monitor.HasValue) { - return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor {1}.", layout, monitor.Value); + return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_monitor, layout, monitor.Value); } - return string.Format(CultureInfo.InvariantCulture, "Layout '{0}' applied to monitor 1.", layout); + return string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_success_default, layout); } private static (CustomLayouts.CustomLayoutWrapper? TargetCustomLayout, LayoutTemplates.TemplateLayoutWrapper? TargetTemplate) ResolveTargetLayout(string layout) @@ -127,10 +127,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand if (!targetCustomLayout.HasValue && !targetTemplate.HasValue) { - throw new InvalidOperationException( - $"Layout '{layout}' not found{Environment.NewLine}" + - "Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid')" + - $"{Environment.NewLine} For custom layouts, use the UUID from 'get-layouts'"); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_not_found, layout)); } return (targetCustomLayout, targetTemplate); @@ -197,7 +194,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand int monitorIndex = monitor.Value - 1; // Convert to 0-based. if (monitorIndex < 0 || monitorIndex >= editorParams.Monitors.Count) { - throw new InvalidOperationException($"Monitor {monitor.Value} not found. Available monitors: 1-{editorParams.Monitors.Count}"); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_monitor_not_found, monitor.Value, editorParams.Monitors.Count)); } result.Add(monitorIndex); @@ -250,7 +247,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand if (newLayouts.Count == 0) { - throw new InvalidOperationException("Internal error - no monitors to update."); + throw new InvalidOperationException(Properties.Resources.set_layout_error_no_monitors); } return newLayouts; @@ -306,7 +303,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand } else { - throw new InvalidOperationException($"Unsupported custom layout type '{targetCustomLayout.Value.Type}'."); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.set_layout_error_unsupported_type, targetCustomLayout.Value.Type)); } return ( @@ -329,7 +326,7 @@ internal sealed partial class SetLayoutCommand : FancyZonesBaseCommand targetTemplate.Value.SensitivityRadius); } - throw new InvalidOperationException("Internal error - no layout selected."); + throw new InvalidOperationException(Properties.Resources.set_layout_error_no_layout); } private static AppliedLayouts.AppliedLayoutsListWrapper MergeWithHistoricalLayouts( diff --git a/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj b/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj index 36274c95ef..f1198d5b40 100644 --- a/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj +++ b/src/modules/fancyzones/FancyZonesCLI/FancyZonesCLI.csproj @@ -13,7 +13,7 @@ false ..\..\..\..\$(Platform)\$(Configuration) FancyZonesCLI - $(NoWarn);SA1500;SA1402;CA1852 + $(NoWarn);SA1500;SA1402;CA1852;CA1863;CA1305 @@ -24,6 +24,22 @@ + + + + + + Resources.resx + True + True + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + diff --git a/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs b/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..3df0a9c606 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.Designer.cs @@ -0,0 +1,353 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace FancyZonesCLI.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FancyZonesCLI.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + internal static string error_fancyzones_not_running { + get { + return ResourceManager.GetString("error_fancyzones_not_running", resourceCulture); + } + } + + internal static string cmd_get_active_layout { + get { + return ResourceManager.GetString("cmd_get_active_layout", resourceCulture); + } + } + + internal static string get_active_layout_no_monitor_info { + get { + return ResourceManager.GetString("get_active_layout_no_monitor_info", resourceCulture); + } + } + + internal static string get_active_layout_no_layouts { + get { + return ResourceManager.GetString("get_active_layout_no_layouts", resourceCulture); + } + } + + internal static string get_active_layout_header { + get { + return ResourceManager.GetString("get_active_layout_header", resourceCulture); + } + } + + internal static string get_active_layout_no_layout { + get { + return ResourceManager.GetString("get_active_layout_no_layout", resourceCulture); + } + } + + internal static string cmd_get_layouts { + get { + return ResourceManager.GetString("cmd_get_layouts", resourceCulture); + } + } + + internal static string get_layouts_templates_header { + get { + return ResourceManager.GetString("get_layouts_templates_header", resourceCulture); + } + } + + internal static string get_layouts_custom_header { + get { + return ResourceManager.GetString("get_layouts_custom_header", resourceCulture); + } + } + + internal static string get_layouts_canvas_note { + get { + return ResourceManager.GetString("get_layouts_canvas_note", resourceCulture); + } + } + + internal static string get_layouts_canvas_detail { + get { + return ResourceManager.GetString("get_layouts_canvas_detail", resourceCulture); + } + } + + internal static string get_layouts_usage { + get { + return ResourceManager.GetString("get_layouts_usage", resourceCulture); + } + } + + internal static string cmd_get_monitors { + get { + return ResourceManager.GetString("cmd_get_monitors", resourceCulture); + } + } + + internal static string get_monitors_error { + get { + return ResourceManager.GetString("get_monitors_error", resourceCulture); + } + } + + internal static string get_monitors_no_monitors { + get { + return ResourceManager.GetString("get_monitors_no_monitors", resourceCulture); + } + } + + internal static string get_monitors_header { + get { + return ResourceManager.GetString("get_monitors_header", resourceCulture); + } + } + + internal static string cmd_set_layout { + get { + return ResourceManager.GetString("cmd_set_layout", resourceCulture); + } + } + + internal static string set_layout_arg_layout { + get { + return ResourceManager.GetString("set_layout_arg_layout", resourceCulture); + } + } + + internal static string set_layout_opt_monitor { + get { + return ResourceManager.GetString("set_layout_opt_monitor", resourceCulture); + } + } + + internal static string set_layout_opt_all { + get { + return ResourceManager.GetString("set_layout_opt_all", resourceCulture); + } + } + + internal static string set_layout_error_monitor_index { + get { + return ResourceManager.GetString("set_layout_error_monitor_index", resourceCulture); + } + } + + internal static string set_layout_error_both_options { + get { + return ResourceManager.GetString("set_layout_error_both_options", resourceCulture); + } + } + + internal static string set_layout_error_not_found { + get { + return ResourceManager.GetString("set_layout_error_not_found", resourceCulture); + } + } + + internal static string set_layout_error_monitor_not_found { + get { + return ResourceManager.GetString("set_layout_error_monitor_not_found", resourceCulture); + } + } + + internal static string set_layout_error_no_monitors { + get { + return ResourceManager.GetString("set_layout_error_no_monitors", resourceCulture); + } + } + + internal static string set_layout_error_unsupported_type { + get { + return ResourceManager.GetString("set_layout_error_unsupported_type", resourceCulture); + } + } + + internal static string set_layout_error_no_layout { + get { + return ResourceManager.GetString("set_layout_error_no_layout", resourceCulture); + } + } + + internal static string set_layout_success_all { + get { + return ResourceManager.GetString("set_layout_success_all", resourceCulture); + } + } + + internal static string set_layout_success_monitor { + get { + return ResourceManager.GetString("set_layout_success_monitor", resourceCulture); + } + } + + internal static string set_layout_success_default { + get { + return ResourceManager.GetString("set_layout_success_default", resourceCulture); + } + } + + internal static string cmd_open_editor { + get { + return ResourceManager.GetString("cmd_open_editor", resourceCulture); + } + } + + internal static string open_editor_error { + get { + return ResourceManager.GetString("open_editor_error", resourceCulture); + } + } + + internal static string cmd_open_settings { + get { + return ResourceManager.GetString("cmd_open_settings", resourceCulture); + } + } + + internal static string open_settings_error_not_started { + get { + return ResourceManager.GetString("open_settings_error_not_started", resourceCulture); + } + } + + internal static string open_settings_error { + get { + return ResourceManager.GetString("open_settings_error", resourceCulture); + } + } + + internal static string cmd_set_hotkey { + get { + return ResourceManager.GetString("cmd_set_hotkey", resourceCulture); + } + } + + internal static string set_hotkey_arg_key { + get { + return ResourceManager.GetString("set_hotkey_arg_key", resourceCulture); + } + } + + internal static string set_hotkey_arg_layout { + get { + return ResourceManager.GetString("set_hotkey_arg_layout", resourceCulture); + } + } + + internal static string set_hotkey_error_invalid_key { + get { + return ResourceManager.GetString("set_hotkey_error_invalid_key", resourceCulture); + } + } + + internal static string set_hotkey_error_not_custom { + get { + return ResourceManager.GetString("set_hotkey_error_not_custom", resourceCulture); + } + } + + internal static string cmd_remove_hotkey { + get { + return ResourceManager.GetString("cmd_remove_hotkey", resourceCulture); + } + } + + internal static string remove_hotkey_arg_key { + get { + return ResourceManager.GetString("remove_hotkey_arg_key", resourceCulture); + } + } + + internal static string remove_hotkey_no_hotkeys { + get { + return ResourceManager.GetString("remove_hotkey_no_hotkeys", resourceCulture); + } + } + + internal static string remove_hotkey_not_found { + get { + return ResourceManager.GetString("remove_hotkey_not_found", resourceCulture); + } + } + + internal static string cmd_get_hotkeys { + get { + return ResourceManager.GetString("cmd_get_hotkeys", resourceCulture); + } + } + + internal static string get_hotkeys_no_hotkeys { + get { + return ResourceManager.GetString("get_hotkeys_no_hotkeys", resourceCulture); + } + } + + internal static string get_hotkeys_header { + get { + return ResourceManager.GetString("get_hotkeys_header", resourceCulture); + } + } + + internal static string get_hotkeys_instruction { + get { + return ResourceManager.GetString("get_hotkeys_instruction", resourceCulture); + } + } + + internal static string editor_params_timeout { + get { + return ResourceManager.GetString("editor_params_timeout", resourceCulture); + } + } + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx b/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx new file mode 100644 index 0000000000..5d4d0a1c74 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Properties/Resources.resx @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + Error: FancyZones is not running. Start PowerToys (FancyZones) and retry. + + + + + Show currently active layout + + + Could not get current monitor information. + + + No layouts configured. + + + === Active FancyZones Layout(s) === + + + No layout applied + + + + + List available layouts + + + === Built-in Template Layouts ({0} total) === + + + === Custom Layouts ({0} total) === + + + Note: Canvas layout preview is approximate. + + + Open FancyZones Editor for precise zone boundaries. + + + Use 'FancyZonesCLI.exe set-layout <UUID>' to apply a layout. + + + + + List monitors and FancyZones metadata + + + Failed to read monitor information. {0} +Note: Ensure FancyZones is running to get current monitor information. + + + No monitors found. + + + === Monitors ({0} total) === + + + + + Set layout by UUID or template name + + + Layout UUID or template type (e.g. focus, columns) + + + Apply to monitor N (1-based) + + + Apply to all monitors + + + Monitor index must be >= 1. + + + Cannot specify both --monitor and --all. + + + Layout '{0}' not found +Tip: For templates, use the type name (e.g., 'focus', 'columns', 'rows', 'grid', 'priority-grid') + For custom layouts, use the UUID from 'get-layouts' + + + Monitor {0} not found. Available monitors: 1-{1} + + + Internal error - no monitors to update. + + + Unsupported custom layout type '{0}'. + + + Internal error - no layout selected. + + + Layout '{0}' applied to all monitors. + + + Layout '{0}' applied to monitor {1}. + + + Layout '{0}' applied to monitor 1. + + + + + Launch FancyZones layout editor + + + Failed to request FancyZones Editor launch. {0} + + + + + Open FancyZones settings page + + + PowerToys.exe failed to start. + + + Failed to open FancyZones Settings. {0} + + + + + Assign hotkey (0-9) to a custom layout + + + Hotkey index (0-9) + + + Custom layout UUID + + + Key must be between 0 and 9. + + + Layout '{0}' is not a custom layout UUID. + + + + + Remove hotkey assignment + + + Hotkey index (0-9) + + + No hotkeys configured. + + + No hotkey assigned to key {0} + + + + + List all layout hotkeys + + + No hotkeys configured. + + + === Layout Hotkeys === + + + Press Win + Ctrl + Alt + <number> to switch layouts: + + + + + Could not get current monitor information (timed out after {0}ms waiting for '{1}'). + + diff --git a/src/modules/fancyzones/FancyZonesCLI/Telemetry/FancyZonesCLICommandEvent.cs b/src/modules/fancyzones/FancyZonesCLI/Telemetry/FancyZonesCLICommandEvent.cs new file mode 100644 index 0000000000..05751b5f43 --- /dev/null +++ b/src/modules/fancyzones/FancyZonesCLI/Telemetry/FancyZonesCLICommandEvent.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Tracing; +using Microsoft.PowerToys.Telemetry; +using Microsoft.PowerToys.Telemetry.Events; + +namespace FancyZonesCLI.Telemetry +{ + /// + /// Telemetry event for FancyZones CLI command execution. + /// + [EventData] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + public class FancyZonesCLICommandEvent : EventBase, IEvent + { + public FancyZonesCLICommandEvent() + { + EventName = "FancyZones_CLICommand"; + } + + /// + /// Gets or sets the name of the CLI command that was executed. + /// + public string CommandName { get; set; } + + /// + /// Gets or sets a value indicating whether the command executed successfully. + /// + public bool Successful { get; set; } + + public PartA_PrivTags PartA_PrivTags => PartA_PrivTags.ProductAndServiceUsage; + } +} diff --git a/src/modules/fancyzones/FancyZonesCLI/Utils/EditorParametersRefresh.cs b/src/modules/fancyzones/FancyZonesCLI/Utils/EditorParametersRefresh.cs index 898d4b2f9e..44303fb50a 100644 --- a/src/modules/fancyzones/FancyZonesCLI/Utils/EditorParametersRefresh.cs +++ b/src/modules/fancyzones/FancyZonesCLI/Utils/EditorParametersRefresh.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Globalization; using System.IO; using System.Text.Json; using System.Threading; @@ -60,7 +61,7 @@ internal static class EditorParametersRefresh var finalParams = FancyZonesDataIO.ReadEditorParameters(); if (finalParams.Monitors == null || finalParams.Monitors.Count == 0) { - throw new InvalidOperationException($"Could not get current monitor information (timed out after {maxWaitMilliseconds}ms waiting for '{Path.GetFileName(filePath)}')."); + throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Properties.Resources.editor_params_timeout, maxWaitMilliseconds, Path.GetFileName(filePath))); } return finalParams;