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 }