/ src / dsc / v3 / PowerToys.DSC / DSCResources / SettingsResource.cs
SettingsResource.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Collections.Generic;
  7  using System.Diagnostics;
  8  using System.Globalization;
  9  using System.IO;
 10  using System.Linq;
 11  using System.Text;
 12  using ManagedCommon;
 13  using Microsoft.PowerToys.Settings.UI.Library;
 14  using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
 15  using PowerToys.DSC.Models;
 16  using PowerToys.DSC.Models.FunctionData;
 17  using PowerToys.DSC.Properties;
 18  
 19  namespace PowerToys.DSC.DSCResources;
 20  
 21  /// <summary>
 22  /// Represents the DSC resource for managing PowerToys settings.
 23  /// </summary>
 24  public sealed class SettingsResource : BaseResource
 25  {
 26      private static readonly CompositeFormat FailedToWriteManifests = CompositeFormat.Parse(Resources.FailedToWriteManifests);
 27  
 28      public const string AppModule = "App";
 29      public const string ResourceName = "settings";
 30  
 31      private readonly Dictionary<string, Func<string?, ISettingsFunctionData>> _moduleFunctionData;
 32  
 33      public string ModuleOrDefault => string.IsNullOrEmpty(Module) ? AppModule : Module;
 34  
 35      public SettingsResource(string? module)
 36          : base(ResourceName, module)
 37      {
 38          _moduleFunctionData = new()
 39          {
 40              { AppModule,                                    CreateModuleFunctionData<GeneralSettings> },
 41              { nameof(ModuleType.AdvancedPaste),             CreateModuleFunctionData<AdvancedPasteSettings> },
 42              { nameof(ModuleType.AlwaysOnTop),               CreateModuleFunctionData<AlwaysOnTopSettings> },
 43              { nameof(ModuleType.Awake),                     CreateModuleFunctionData<AwakeSettings> },
 44              { nameof(ModuleType.ColorPicker),               CreateModuleFunctionData<ColorPickerSettings> },
 45              { nameof(ModuleType.CropAndLock),               CreateModuleFunctionData<CropAndLockSettings> },
 46              { nameof(ModuleType.EnvironmentVariables),      CreateModuleFunctionData<EnvironmentVariablesSettings> },
 47              { nameof(ModuleType.FancyZones),                CreateModuleFunctionData<FancyZonesSettings> },
 48              { nameof(ModuleType.FileLocksmith),             CreateModuleFunctionData<FileLocksmithSettings> },
 49              { nameof(ModuleType.FindMyMouse),               CreateModuleFunctionData<FindMyMouseSettings> },
 50              { nameof(ModuleType.Hosts),                     CreateModuleFunctionData<HostsSettings> },
 51              { nameof(ModuleType.ImageResizer),              CreateModuleFunctionData<ImageResizerSettings> },
 52              { nameof(ModuleType.KeyboardManager),           CreateModuleFunctionData<KeyboardManagerSettings> },
 53              { nameof(ModuleType.MouseHighlighter),          CreateModuleFunctionData<MouseHighlighterSettings> },
 54              { nameof(ModuleType.MouseJump),                 CreateModuleFunctionData<MouseJumpSettings> },
 55              { nameof(ModuleType.MousePointerCrosshairs),    CreateModuleFunctionData<MousePointerCrosshairsSettings> },
 56              { nameof(ModuleType.Peek),                      CreateModuleFunctionData<PeekSettings> },
 57              { nameof(ModuleType.PowerRename),               CreateModuleFunctionData<PowerRenameSettings> },
 58              { nameof(ModuleType.PowerAccent),               CreateModuleFunctionData<PowerAccentSettings> },
 59              { nameof(ModuleType.RegistryPreview),           CreateModuleFunctionData<RegistryPreviewSettings> },
 60              { nameof(ModuleType.MeasureTool),               CreateModuleFunctionData<MeasureToolSettings> },
 61              { nameof(ModuleType.ShortcutGuide),             CreateModuleFunctionData<ShortcutGuideSettings> },
 62              { nameof(ModuleType.PowerOCR),                  CreateModuleFunctionData<PowerOcrSettings> },
 63              { nameof(ModuleType.Workspaces),                CreateModuleFunctionData<WorkspacesSettings> },
 64              { nameof(ModuleType.ZoomIt),                    CreateModuleFunctionData<ZoomItSettings> },
 65  
 66              // The following modules are not currently supported:
 67              // - MouseWithoutBorders    Contains sensitive configuration values, making export/import potentially insecure.
 68              // - PowerLauncher          Uses absolute file paths in its settings, which are not portable across systems.
 69              // - NewPlus                Uses absolute file paths in its settings, which are not portable across systems.
 70          };
 71      }
 72  
 73      /// <inheritdoc/>
 74      public override bool ExportState(string? input)
 75      {
 76          var data = CreateFunctionData();
 77          data.GetState();
 78          WriteJsonOutputLine(data.Output.ToJson());
 79          return true;
 80      }
 81  
 82      /// <inheritdoc/>
 83      public override bool GetState(string? input)
 84      {
 85          return ExportState(input);
 86      }
 87  
 88      /// <inheritdoc/>
 89      public override bool SetState(string? input)
 90      {
 91          if (string.IsNullOrEmpty(input))
 92          {
 93              WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
 94              return false;
 95          }
 96  
 97          var data = CreateFunctionData(input);
 98          data.GetState();
 99  
100          // Capture the diff before updating the output
101          var diff = data.GetDiffJson();
102  
103          // Only call Set if the desired state is different from the current state
104          if (!data.TestState())
105          {
106              var inputSettings = data.Input.SettingsInternal;
107              data.Output.SettingsInternal = inputSettings;
108              data.SetState();
109          }
110  
111          WriteJsonOutputLine(data.Output.ToJson());
112          WriteJsonOutputLine(diff);
113          return true;
114      }
115  
116      /// <inheritdoc/>
117      public override bool TestState(string? input)
118      {
119          if (string.IsNullOrEmpty(input))
120          {
121              WriteMessageOutputLine(DscMessageLevel.Error, Resources.InputEmptyOrNullError);
122              return false;
123          }
124  
125          var data = CreateFunctionData(input);
126          data.GetState();
127          data.Output.InDesiredState = data.TestState();
128  
129          WriteJsonOutputLine(data.Output.ToJson());
130          WriteJsonOutputLine(data.GetDiffJson());
131          return true;
132      }
133  
134      /// <inheritdoc/>
135      public override bool Schema()
136      {
137          var data = CreateFunctionData();
138          WriteJsonOutputLine(data.Schema());
139          return true;
140      }
141  
142      /// <inheritdoc/>
143      /// <remarks>
144      /// If an output directory is specified, write the manifests to files,
145      /// otherwise output them to the console.
146      /// </remarks>
147      public override bool Manifest(string? outputDir)
148      {
149          var manifests = GenerateManifests();
150  
151          if (!string.IsNullOrEmpty(outputDir))
152          {
153              try
154              {
155                  foreach (var (name, manifest) in manifests)
156                  {
157                      File.WriteAllText(Path.Combine(outputDir, $"microsoft.powertoys.{name}.settings.dsc.resource.json"), manifest);
158                  }
159              }
160              catch (Exception ex)
161              {
162                  var errorMessage = string.Format(CultureInfo.InvariantCulture, FailedToWriteManifests, outputDir, ex.Message);
163                  WriteMessageOutputLine(DscMessageLevel.Error, errorMessage);
164                  return false;
165              }
166          }
167          else
168          {
169              foreach (var (_, manifest) in manifests)
170              {
171                  WriteJsonOutputLine(manifest);
172              }
173          }
174  
175          return true;
176      }
177  
178      /// <summary>
179      /// Generates manifests for the specified module or all supported modules
180      /// if no module is specified.
181      /// </summary>
182      /// <returns>A list of tuples containing the module name and its corresponding manifest JSON.</returns>
183      private List<(string Name, string Manifest)> GenerateManifests()
184      {
185          List<(string Name, string Manifest)> manifests = [];
186          if (!string.IsNullOrEmpty(Module))
187          {
188              manifests.Add((Module, GenerateManifest(Module)));
189          }
190          else
191          {
192              foreach (var module in GetSupportedModules())
193              {
194                  manifests.Add((module, GenerateManifest(module)));
195              }
196          }
197  
198          return manifests;
199      }
200  
201      /// <summary>
202      /// Generate a DSC resource JSON manifest for the specified module.
203      /// </summary>
204      /// <param name="module">The name of the module for which to generate the manifest.</param>
205      /// <returns>A JSON string representing the DSC resource manifest.</returns>
206      private string GenerateManifest(string module)
207      {
208          // Note: The description is not localized because the generated
209          // manifest file will be part of the package
210          return new DscManifest($"{module}Settings", "0.1.0")
211              .AddDescription($"Allows management of {module} settings state via the DSC v3 command line interface protocol.")
212              .AddStdinMethod("export", ["export", "--module", module, "--resource", "settings"])
213              .AddStdinMethod("get", ["get", "--module", module, "--resource", "settings"])
214              .AddJsonInputMethod("set", "--input", ["set", "--module", module, "--resource", "settings"], implementsPretest: true, stateAndDiff: true)
215              .AddJsonInputMethod("test", "--input", ["test", "--module", module, "--resource", "settings"], stateAndDiff: true)
216              .AddCommandMethod("schema", ["schema", "--module", module, "--resource", "settings"])
217              .ToJson();
218      }
219  
220      /// <inheritdoc/>
221      public override IList<string> GetSupportedModules()
222      {
223          return [.. _moduleFunctionData.Keys.Order()];
224      }
225  
226      /// <summary>
227      /// Creates the function data for the specified module or the default module if none is specified.
228      /// </summary>
229      /// <param name="input">The input string, if any.</param>
230      /// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified module.</returns>
231      public ISettingsFunctionData CreateFunctionData(string? input = null)
232      {
233          Debug.Assert(_moduleFunctionData.ContainsKey(ModuleOrDefault), "Module should be supported by the resource.");
234          return _moduleFunctionData[ModuleOrDefault](input);
235      }
236  
237      /// <summary>
238      /// Creates the function data for a specific settings configuration type.
239      /// </summary>
240      /// <typeparam name="TSettingsConfig">The type of settings configuration to create function data for.</typeparam>
241      /// <param name="input">The input string, if any.</param>
242      /// <returns>An instance of <see cref="ISettingsFunctionData"/> for the specified settings configuration type.</returns>
243      private ISettingsFunctionData CreateModuleFunctionData<TSettingsConfig>(string? input)
244      where TSettingsConfig : ISettingsConfig, new()
245      {
246          return new SettingsFunctionData<TSettingsConfig>(input);
247      }
248  }