diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt
index 2fd76fc9893..5bc3d299804 100644
--- a/.github/actions/spelling/expect/expect.txt
+++ b/.github/actions/spelling/expect/expect.txt
@@ -1600,6 +1600,7 @@ STDMETHODIMP
STGM
stl
STRINGTABLE
+strnicmp
STRSAFE
STUBHEAD
STUVWX
diff --git a/OpenConsole.slnx b/OpenConsole.slnx
index 9dff529afa7..8da2e13d541 100644
--- a/OpenConsole.slnx
+++ b/OpenConsole.slnx
@@ -687,6 +687,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -723,6 +735,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
index 317109ed354..7e9a15bee17 100644
--- a/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
+++ b/src/cascadia/CascadiaPackage/CascadiaPackage.wapproj
@@ -89,6 +89,9 @@
{416fd703-baa7-4f6e-9361-64f550ec8fca}
+
+ {CA5CAD1A-1C2D-46EF-B109-22320F070B04}
+
{71CC9D78-BA29-4D93-946F-BEF5D9A3A6EF}
diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
index 8335a59f420..6db0dd9b706 100644
--- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest
@@ -1,157 +1,175 @@
-
-
-
-
-
-
-
- ms-resource:AppStoreNameCan
- Microsoft Corporation
- Images\StoreLogo.png
-
- disabled
-
-
-
- HKEY_CURRENT_USER\Console\%%Startup
-
-
- defer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- com.microsoft.windows.terminal.settings
-
-
-
-
-
-
-
-
- {A854D02A-F2FE-44A5-BB24-D03F4CF830D4}
-
-
-
-
-
-
- {1706609C-A4CE-4C0D-B7D2-C19BF66398A5}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ ms-resource:AppStoreNameCan
+ Microsoft Corporation
+ Images\StoreLogo.png
+
+ disabled
+
+
+
+ HKEY_CURRENT_USER\Console\%%Startup
+
+
+ defer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.microsoft.windows.terminal.settings
+
+
+
+
+
+
+
+
+ {A854D02A-F2FE-44A5-BB24-D03F4CF830D4}
+
+
+
+
+
+
+ {1706609C-A4CE-4C0D-B7D2-C19BF66398A5}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
index 4a3153076c8..8ac9311f61f 100644
--- a/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Dev.appxmanifest
@@ -1,152 +1,170 @@
-
-
-
-
-
-
-
- ms-resource:AppStoreNameDev
- A Lone Developer
- Images\StoreLogo.png
-
- disabled
-
-
-
- HKEY_CURRENT_USER\Console\%%Startup
-
-
- defer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- com.microsoft.windows.terminal.settings
-
-
-
-
-
- {1F9F2BF5-5BC3-4F17-B0E6-912413F1F451}
-
-
-
-
-
-
- {051F34EE-C1FD-4B19-AF75-9BA54648434C}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ ms-resource:AppStoreNameDev
+ A Lone Developer
+ Images\StoreLogo.png
+
+ disabled
+
+
+
+ HKEY_CURRENT_USER\Console\%%Startup
+
+
+ defer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.microsoft.windows.terminal.settings
+
+
+
+
+
+ {1F9F2BF5-5BC3-4F17-B0E6-912413F1F451}
+
+
+
+
+
+
+ {051F34EE-C1FD-4B19-AF75-9BA54648434C}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
index 497923da40a..6f8d55c23c3 100644
--- a/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package-Pre.appxmanifest
@@ -1,258 +1,276 @@
-
-
-
-
-
-
-
- ms-resource:AppStoreNamePre
- Microsoft Corporation
- Images\StoreLogo.png
-
- disabled
-
-
-
- HKEY_CURRENT_USER\Console\%%Startup
-
-
- defer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- com.microsoft.windows.terminal.settings
-
-
-
-
-
-
-
-
- {06EC847C-C0A5-46B8-92CB-7C92F6E35CD5}
-
-
-
-
-
-
- {86633F1F-6454-40EC-89CE-DA4EBA977EE2}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ ms-resource:AppStoreNamePre
+ Microsoft Corporation
+ Images\StoreLogo.png
+
+ disabled
+
+
+
+ HKEY_CURRENT_USER\Console\%%Startup
+
+
+ defer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.microsoft.windows.terminal.settings
+
+
+
+
+
+
+
+
+ {06EC847C-C0A5-46B8-92CB-7C92F6E35CD5}
+
+
+
+
+
+
+ {86633F1F-6454-40EC-89CE-DA4EBA977EE2}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/CascadiaPackage/Package.appxmanifest b/src/cascadia/CascadiaPackage/Package.appxmanifest
index 679f3b9a526..02d48e9044a 100644
--- a/src/cascadia/CascadiaPackage/Package.appxmanifest
+++ b/src/cascadia/CascadiaPackage/Package.appxmanifest
@@ -1,258 +1,276 @@
-
-
-
-
-
-
-
- ms-resource:AppStoreName
- Microsoft Corporation
- Images\StoreLogo.png
-
- disabled
-
-
-
- HKEY_CURRENT_USER\Console\%%Startup
-
-
- defer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- com.microsoft.windows.terminal.settings
-
-
-
-
-
-
-
-
- {2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}
-
-
-
-
-
-
- {E12CFF52-A866-4C77-9A90-F570A7AA2C6B}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+ ms-resource:AppStoreName
+ Microsoft Corporation
+ Images\StoreLogo.png
+
+ disabled
+
+
+
+ HKEY_CURRENT_USER\Console\%%Startup
+
+
+ defer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ com.microsoft.windows.terminal.settings
+
+
+
+
+
+
+
+
+ {2EACA947-7F5F-4CFA-BA87-8F7FBEEFBE69}
+
+
+
+
+
+
+ {E12CFF52-A866-4C77-9A90-F570A7AA2C6B}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/CascadiaResources.build.items b/src/cascadia/CascadiaResources.build.items
index 336fad5048b..e9bc7fb0132 100644
--- a/src/cascadia/CascadiaResources.build.items
+++ b/src/cascadia/CascadiaResources.build.items
@@ -31,6 +31,11 @@
true
%(RecursiveDir)%(FileName)%(Extension)
+
+
+ true
+ %(FileName)%(Extension)
+
diff --git a/src/cascadia/TerminalDsc/Resource/CommandBuilder.cpp b/src/cascadia/TerminalDsc/Resource/CommandBuilder.cpp
new file mode 100644
index 00000000000..66783e1e125
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/CommandBuilder.cpp
@@ -0,0 +1,250 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "CommandBuilder.h"
+#include "JsonUtils.h"
+#include "Logger.h"
+#include "ManifestGenerator.h"
+#include "ResourceExecutor.h"
+
+#include
+#include
+
+using namespace Microsoft::Terminal::Dsc;
+
+namespace
+{
+ constexpr std::string_view usage{ "usage: TerminalDsc.exe [--resource ] [--input ] [--what-if] [--save]" };
+
+ struct ParsedArgs
+ {
+ std::string operation;
+ std::optional resourceType;
+ std::optional input;
+ bool whatIf = false;
+ bool save = false;
+ };
+
+ std::optional parseArgs(const std::vector& args)
+ {
+ if (args.empty())
+ {
+ Logger::WriteError(usage);
+ return std::nullopt;
+ }
+
+ ParsedArgs parsed;
+ parsed.operation = til::u16u8(args[0]);
+
+ for (size_t i = 1; i < args.size(); ++i)
+ {
+ const auto& arg{ args[i] };
+ if ((arg == L"--resource" || arg == L"-r") && i + 1 < args.size())
+ {
+ parsed.resourceType = til::u16u8(args[++i]);
+ }
+ else if ((arg == L"--input" || arg == L"-i") && i + 1 < args.size())
+ {
+ parsed.input = til::u16u8(args[++i]);
+ }
+ else if (arg == L"--what-if" || arg == L"-w")
+ {
+ parsed.whatIf = true;
+ }
+ else if (arg == L"--save")
+ {
+ parsed.save = true;
+ }
+ else
+ {
+ Logger::WriteError(fmt::format(FMT_COMPILE("unexpected argument '{}'; {}"), til::u16u8(arg), usage));
+ return std::nullopt;
+ }
+ }
+ return parsed;
+ }
+
+ // The stem the generated manifests reference as "executable"; PATH lookup
+ // appends .exe on Windows.
+ std::string executableStem()
+ {
+ const std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) };
+ return module.stem().string();
+ }
+
+ int runManifest(const ResourceRegistry& registry, const ParsedArgs& parsed, std::ostream& output)
+ {
+ const auto stem{ executableStem() };
+
+ // With several resources, --resource narrows the output to one manifest.
+ Json::Value document;
+ if (parsed.resourceType && !registry.IsSingleResource())
+ {
+ const auto registration{ registry.Find(*parsed.resourceType) };
+ if (!registration)
+ {
+ Logger::WriteError(fmt::format(FMT_COMPILE("unknown resource '{}'"), *parsed.resourceType));
+ return ExitError;
+ }
+ document = ManifestGenerator::BuildResourceManifest(*registration, false, stem);
+ }
+ else
+ {
+ document = ManifestGenerator::BuildManifestDocument(registry, stem);
+ }
+
+ if (parsed.save)
+ {
+ // Write next to the executable, where Microsoft DSC discovers it.
+ const std::filesystem::path module{ wil::GetModuleFileNameW(nullptr) };
+ const auto path{ module.parent_path() / ManifestGenerator::ManifestFileName(registry, stem) };
+
+ Json::StreamWriterBuilder builder;
+ builder.settings_["indentation"] = " ";
+ builder.settings_["commentStyle"] = "None";
+
+ std::ofstream file{ path, std::ios::binary };
+ if (!file)
+ {
+ throw DscResourceError{ fmt::format(FMT_COMPILE("failed to write manifest to '{}'"), path.string()) };
+ }
+ file << Json::writeString(builder, document) << '\n';
+ Logger::WriteInfo(fmt::format(FMT_COMPILE("manifest saved to '{}'"), path.string()));
+ }
+ else
+ {
+ output << SerializeJson(document) << '\n';
+ }
+ return ExitSuccess;
+ }
+}
+
+namespace Microsoft::Terminal::Dsc
+{
+ int CommandBuilder::Run(const std::vector& args, std::ostream& output)
+ {
+ const auto parsed{ parseArgs(args) };
+ if (!parsed)
+ {
+ return ExitError;
+ }
+
+ static constexpr std::array knownOperations{ "get", "set", "test", "delete", "export", "schema", "manifest" };
+ if (std::ranges::find(knownOperations, parsed->operation) == knownOperations.end())
+ {
+ Logger::WriteError(fmt::format(FMT_COMPILE("unknown operation '{}'; {}"), parsed->operation, usage));
+ return ExitError;
+ }
+
+ try
+ {
+ if (parsed->operation == "manifest")
+ {
+ return runManifest(_registry, *parsed, output);
+ }
+
+ // Resolve the target resource. With a single registration the
+ // --resource selector is implicit but, when given, must match.
+ const ResourceRegistration* registration{ nullptr };
+ if (parsed->resourceType)
+ {
+ registration = _registry.Find(*parsed->resourceType);
+ if (!registration)
+ {
+ Logger::WriteError(fmt::format(FMT_COMPILE("unknown resource '{}'"), *parsed->resourceType));
+ return ExitError;
+ }
+ }
+ else if (_registry.IsSingleResource())
+ {
+ registration = &_registry.All().front();
+ }
+ else
+ {
+ Logger::WriteError(fmt::format(FMT_COMPILE("--resource is required; {}"), usage));
+ return ExitError;
+ }
+
+ if (parsed->whatIf)
+ {
+ // No registered resource implements what-if yet; DSC
+ // synthesizes it from test/get for those that don't.
+ Logger::WriteError(fmt::format(FMT_COMPILE("resource '{}' does not support --what-if"), registration->Metadata().type));
+ return ExitError;
+ }
+
+ const auto notSupported = [&](std::string_view operation) {
+ Logger::WriteError(fmt::format(FMT_COMPILE("resource '{}' does not support the '{}' operation"), registration->Metadata().type, operation));
+ return ExitError;
+ };
+
+ if (parsed->operation == "get")
+ {
+ if (!registration->get)
+ {
+ return notSupported("get");
+ }
+ ResourceExecutor::ExecuteGet(*registration, parsed->input, output);
+ }
+ else if (parsed->operation == "set")
+ {
+ if (!registration->set)
+ {
+ return notSupported("set");
+ }
+ ResourceExecutor::ExecuteSet(*registration, parsed->input, output);
+ }
+ else if (parsed->operation == "test")
+ {
+ if (!registration->test)
+ {
+ return notSupported("test");
+ }
+ ResourceExecutor::ExecuteTest(*registration, parsed->input, output);
+ }
+ else if (parsed->operation == "delete")
+ {
+ if (!registration->del)
+ {
+ return notSupported("delete");
+ }
+ ResourceExecutor::ExecuteDelete(*registration, parsed->input, output);
+ }
+ else if (parsed->operation == "export")
+ {
+ if (!registration->exp)
+ {
+ return notSupported("export");
+ }
+ ResourceExecutor::ExecuteExport(*registration, parsed->input, output);
+ }
+ else if (parsed->operation == "schema")
+ {
+ ResourceExecutor::ExecuteSchema(*registration, output);
+ }
+ }
+ catch (const DscInputError& e)
+ {
+ Logger::WriteError(e.what());
+ return ExitInvalidInput;
+ }
+ catch (const DscResourceError& e)
+ {
+ Logger::WriteError(e.what());
+ return ExitResourceError;
+ }
+ catch (const winrt::hresult_error& e)
+ {
+ Logger::WriteError(til::u16u8(e.message()));
+ return ExitResourceError;
+ }
+ catch (const std::exception& e)
+ {
+ Logger::WriteError(e.what());
+ return ExitError;
+ }
+
+ return ExitSuccess;
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/CommandBuilder.h b/src/cascadia/TerminalDsc/Resource/CommandBuilder.h
new file mode 100644
index 00000000000..9a21d9f7833
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/CommandBuilder.h
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "ResourceRegistry.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // CommandBuilder:
+ //
+ // TerminalDsc.exe
+ // [--resource ] [--input ] [--what-if] [--save]
+ //
+ // While exactly one resource is registered, --resource is implicit; with
+ // two or more it is required (except on `manifest`, which then emits the
+ // whole manifest list).
+ class CommandBuilder
+ {
+ public:
+ explicit CommandBuilder(ResourceRegistry registry) noexcept :
+ _registry{ std::move(registry) }
+ {
+ }
+
+ // Runs the command line and returns the process exit code. Protocol
+ // output goes to `output`; diagnostics go to stderr via Logger.
+ int Run(const std::vector& args, std::ostream& output);
+
+ private:
+ ResourceRegistry _registry;
+ };
+}
diff --git a/src/cascadia/TerminalDsc/Resource/DscResource.h b/src/cascadia/TerminalDsc/Resource/DscResource.h
new file mode 100644
index 00000000000..274dac2ea7e
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/DscResource.h
@@ -0,0 +1,85 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "ResourceMetadata.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Exit codes shared by every resource served from this executable. These
+ // are the floor; each resource documents its full set in its
+ // ResourceMetadata and thereby in the generated manifest.
+ inline constexpr int ExitSuccess = 0;
+ inline constexpr int ExitError = 1;
+ inline constexpr int ExitInvalidInput = 2;
+ inline constexpr int ExitResourceError = 3;
+
+ // The desired state handed to us was malformed (bad JSON, wrong property
+ // types, unknown properties). Maps to ExitInvalidInput.
+ struct DscInputError : std::runtime_error
+ {
+ using std::runtime_error::runtime_error;
+ };
+
+ // Reading or writing the actual state failed. Maps to ExitResourceError.
+ struct DscResourceError : std::runtime_error
+ {
+ using std::runtime_error::runtime_error;
+ };
+
+ // A Microsoft DSC resource served by this executable. Which operations a
+ // resource supports is expressed by additionally deriving from the
+ // capability interfaces below; the registry detects them at compile time
+ // during registration, and both the command dispatcher and the manifest
+ // generator read from that single source of truth.
+ struct IDscResource
+ {
+ virtual ~IDscResource() = default;
+
+ virtual const ResourceMetadata& Metadata() const noexcept = 0;
+
+ // Returns the JSON schema describing the resource's properties. This
+ // is the source of truth for the manifest's embedded schema.
+ virtual Json::Value Schema() const = 0;
+ };
+
+ // Capability: "get". The instance carries any key/filter properties the
+ // caller provided; resources without key properties may ignore it.
+ struct IGettable
+ {
+ virtual ~IGettable() = default;
+ virtual Json::Value Get(const std::optional& instance) = 0;
+ };
+
+ // Capability: "set". Returns the final state (SetReturn::State) which the
+ // executor writes to stdout.
+ struct ISettable
+ {
+ virtual ~ISettable() = default;
+ virtual Json::Value Set(const Json::Value& instance) = 0;
+ };
+
+ // Capability: "test". Only needed when DSC's synthetic test (get + diff)
+ // is not good enough for the resource's semantics.
+ struct ITestable
+ {
+ virtual ~ITestable() = default;
+ virtual Json::Value Test(const Json::Value& instance) = 0;
+ };
+
+ // Capability: "delete".
+ struct IDeletable
+ {
+ virtual ~IDeletable() = default;
+ virtual void Delete(const std::optional& instance) = 0;
+ };
+
+ // Capability: "export". Returns all instances; single-instance resources
+ // return exactly their current state.
+ struct IExportable
+ {
+ virtual ~IExportable() = default;
+ virtual std::vector Export(const std::optional& filter) = 0;
+ };
+}
diff --git a/src/cascadia/TerminalDsc/Resource/JsonUtils.cpp b/src/cascadia/TerminalDsc/Resource/JsonUtils.cpp
new file mode 100644
index 00000000000..5a034abb937
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/JsonUtils.cpp
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "JsonUtils.h"
+#include "DscResource.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ std::string SerializeJson(const Json::Value& value)
+ {
+ Json::StreamWriterBuilder builder;
+ builder.settings_["indentation"] = "";
+ builder.settings_["commentStyle"] = "None";
+ return Json::writeString(builder, value);
+ }
+
+ Json::Value ParseJson(std::string_view text)
+ {
+ Json::CharReaderBuilder builder;
+ Json::CharReaderBuilder::strictMode(&builder.settings_);
+ const std::unique_ptr reader{ builder.newCharReader() };
+ Json::Value root;
+ std::string errors;
+ if (!reader->parse(text.data(), text.data() + text.size(), &root, &errors))
+ {
+ throw DscInputError{ fmt::format(FMT_COMPILE("invalid JSON input: {}"), errors) };
+ }
+ return root;
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/JsonUtils.h b/src/cascadia/TerminalDsc/Resource/JsonUtils.h
new file mode 100644
index 00000000000..6818728424b
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/JsonUtils.h
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Serializes a JSON value into a compact single line, the shape Microsoft
+ // DSC expects on stdout.
+ std::string SerializeJson(const Json::Value& value);
+
+ // Parses strict JSON; throws DscInputError on malformed input.
+ Json::Value ParseJson(std::string_view text);
+}
diff --git a/src/cascadia/TerminalDsc/Resource/Logger.cpp b/src/cascadia/TerminalDsc/Resource/Logger.cpp
new file mode 100644
index 00000000000..b36035c630c
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/Logger.cpp
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "Logger.h"
+#include "JsonUtils.h"
+
+namespace Microsoft::Terminal::Dsc::Logger
+{
+ namespace
+ {
+ void writeLine(std::string_view level, std::string_view message)
+ {
+ Json::Value line{ Json::objectValue };
+ line[std::string{ level }] = std::string{ message };
+ std::cerr << SerializeJson(line) << '\n';
+ }
+ }
+
+ void WriteInfo(std::string_view message)
+ {
+ writeLine("info", message);
+ }
+
+ void WriteWarning(std::string_view message)
+ {
+ writeLine("warn", message);
+ }
+
+ void WriteError(std::string_view message)
+ {
+ writeLine("error", message);
+ }
+
+ void WriteTrace(std::string_view message)
+ {
+ writeLine("trace", message);
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/Logger.h b/src/cascadia/TerminalDsc/Resource/Logger.h
new file mode 100644
index 00000000000..99c5fe58e87
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/Logger.h
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+namespace Microsoft::Terminal::Dsc::Logger
+{
+ // Microsoft DSC reads diagnostics from stderr as JSON lines with a single
+ // level key, e.g. {"error":"..."}.
+ void WriteInfo(std::string_view message);
+ void WriteWarning(std::string_view message);
+ void WriteError(std::string_view message);
+ void WriteTrace(std::string_view message);
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ManifestGenerator.cpp b/src/cascadia/TerminalDsc/Resource/ManifestGenerator.cpp
new file mode 100644
index 00000000000..161d0a34317
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ManifestGenerator.cpp
@@ -0,0 +1,141 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ManifestGenerator.h"
+
+namespace Microsoft::Terminal::Dsc::ManifestGenerator
+{
+ namespace
+ {
+ Json::Value buildArgs(std::string_view verb, const ResourceRegistration& registration, bool singleResource, bool inputMandatory)
+ {
+ Json::Value args{ Json::arrayValue };
+ args.append(std::string{ verb });
+ if (!singleResource)
+ {
+ args.append("--resource");
+ args.append(std::string{ registration.Metadata().type });
+ }
+ Json::Value inputArg{ Json::objectValue };
+ inputArg["jsonInputArg"] = "--input";
+ inputArg["mandatory"] = inputMandatory;
+ args.append(inputArg);
+ return args;
+ }
+
+ Json::Value buildMethod(std::string_view verb, const ResourceRegistration& registration, bool singleResource, std::string_view executable, bool inputMandatory)
+ {
+ Json::Value method{ Json::objectValue };
+ method["executable"] = std::string{ executable };
+ method["args"] = buildArgs(verb, registration, singleResource, inputMandatory);
+ return method;
+ }
+
+ std::string_view setReturnString(SetReturn value)
+ {
+ switch (value)
+ {
+ case SetReturn::State:
+ return "state";
+ case SetReturn::StateAndDiff:
+ return "stateAndDiff";
+ default:
+ return {};
+ }
+ }
+ }
+
+ Json::Value BuildResourceManifest(const ResourceRegistration& registration, bool singleResource, std::string_view executable)
+ {
+ const auto& metadata{ registration.Metadata() };
+
+ Json::Value manifest{ Json::objectValue };
+ manifest["$schema"] = "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json";
+ manifest["type"] = std::string{ metadata.type };
+ manifest["version"] = std::string{ metadata.version };
+ manifest["description"] = std::string{ metadata.description };
+
+ if (!metadata.tags.empty())
+ {
+ Json::Value tags{ Json::arrayValue };
+ for (const auto& tag : metadata.tags)
+ {
+ tags.append(std::string{ tag });
+ }
+ manifest["tags"] = tags;
+ }
+
+ if (!metadata.exitCodes.empty())
+ {
+ Json::Value exitCodes{ Json::objectValue };
+ for (const auto& exitCode : metadata.exitCodes)
+ {
+ exitCodes[std::to_string(exitCode.code)] = std::string{ exitCode.description };
+ }
+ manifest["exitCodes"] = exitCodes;
+ }
+
+ if (registration.get)
+ {
+ manifest["get"] = buildMethod("get", registration, singleResource, executable, metadata.getRequiresInput);
+ }
+ if (registration.set)
+ {
+ auto method{ buildMethod("set", registration, singleResource, executable, true) };
+ if (const auto returnKind{ setReturnString(metadata.setReturn) }; !returnKind.empty())
+ {
+ method["return"] = std::string{ returnKind };
+ }
+ manifest["set"] = method;
+ }
+ if (registration.test)
+ {
+ manifest["test"] = buildMethod("test", registration, singleResource, executable, true);
+ }
+ if (registration.del)
+ {
+ manifest["delete"] = buildMethod("delete", registration, singleResource, executable, true);
+ }
+ if (registration.exp)
+ {
+ manifest["export"] = buildMethod("export", registration, singleResource, executable, false);
+ }
+
+ Json::Value schema{ Json::objectValue };
+ schema["embedded"] = registration.resource->Schema();
+ manifest["schema"] = schema;
+
+ return manifest;
+ }
+
+ Json::Value BuildManifestDocument(const ResourceRegistry& registry, std::string_view executable)
+ {
+ if (registry.IsSingleResource())
+ {
+ return BuildResourceManifest(registry.All().front(), true, executable);
+ }
+
+ Json::Value document{ Json::objectValue };
+ Json::Value resources{ Json::arrayValue };
+ for (const auto& registration : registry.All())
+ {
+ resources.append(BuildResourceManifest(registration, false, executable));
+ }
+ document["resources"] = resources;
+ return document;
+ }
+
+ std::string ManifestFileName(const ResourceRegistry& registry, std::string_view executableStem)
+ {
+ if (registry.IsSingleResource())
+ {
+ auto name{ std::string{ registry.All().front().Metadata().type } };
+ std::transform(name.begin(), name.end(), name.begin(), [](char ch) {
+ return ch == '/' ? '.' : til::tolower_ascii(ch);
+ });
+ return name + ".dsc.resource.json";
+ }
+ return std::string{ executableStem } + ".dsc.manifests.json";
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ManifestGenerator.h b/src/cascadia/TerminalDsc/Resource/ManifestGenerator.h
new file mode 100644
index 00000000000..4fc784279c3
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ManifestGenerator.h
@@ -0,0 +1,27 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "ResourceRegistry.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Generates Microsoft DSC resource manifests from the registry, so the
+ // manifest can never drift from what the executable actually implements.
+ namespace ManifestGenerator
+ {
+ // The manifest for a single registration. When singleResource is true
+ // the --resource selector is omitted from the generated args.
+ Json::Value BuildResourceManifest(const ResourceRegistration& registration, bool singleResource, std::string_view executable);
+
+ // The full manifest document: the plain resource manifest when one
+ // resource is registered.
+ Json::Value BuildManifestDocument(const ResourceRegistry& registry, std::string_view executable);
+
+ // The conventional file name for the manifest document:
+ // ".dsc.resource.json" for a single
+ // resource, ".dsc.manifests.json" for several.
+ std::string ManifestFileName(const ResourceRegistry& registry, std::string_view executableStem);
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ResourceExecutor.cpp b/src/cascadia/TerminalDsc/Resource/ResourceExecutor.cpp
new file mode 100644
index 00000000000..41f6b3dcd41
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ResourceExecutor.cpp
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ResourceExecutor.h"
+#include "JsonUtils.h"
+
+namespace Microsoft::Terminal::Dsc::ResourceExecutor
+{
+ namespace
+ {
+ void writeJsonLine(std::ostream& output, const Json::Value& value)
+ {
+ output << SerializeJson(value) << '\n';
+ }
+
+ std::optional parseOptionalInput(const std::optional& input)
+ {
+ if (!input || input->empty())
+ {
+ return std::nullopt;
+ }
+ return ParseJson(*input);
+ }
+
+ Json::Value parseRequiredInput(const std::optional& input, std::string_view operation)
+ {
+ if (!input || input->empty())
+ {
+ throw DscInputError{ fmt::format(FMT_COMPILE("'{}' requires a desired state; pass --input "), operation) };
+ }
+ return ParseJson(*input);
+ }
+ }
+
+ void ExecuteGet(const ResourceRegistration& registration, const std::optional& input, std::ostream& output)
+ {
+ writeJsonLine(output, registration.get->Get(parseOptionalInput(input)));
+ }
+
+ void ExecuteSet(const ResourceRegistration& registration, const std::optional& input, std::ostream& output)
+ {
+ const auto finalState{ registration.set->Set(parseRequiredInput(input, "set")) };
+ if (registration.Metadata().setReturn != SetReturn::None)
+ {
+ writeJsonLine(output, finalState);
+ }
+ }
+
+ void ExecuteTest(const ResourceRegistration& registration, const std::optional& input, std::ostream& output)
+ {
+ writeJsonLine(output, registration.test->Test(parseRequiredInput(input, "test")));
+ }
+
+ void ExecuteDelete(const ResourceRegistration& registration, const std::optional& input, std::ostream& /*output*/)
+ {
+ registration.del->Delete(parseOptionalInput(input));
+ }
+
+ void ExecuteExport(const ResourceRegistration& registration, const std::optional& input, std::ostream& output)
+ {
+ for (const auto& instance : registration.exp->Export(parseOptionalInput(input)))
+ {
+ writeJsonLine(output, instance);
+ }
+ }
+
+ void ExecuteSchema(const ResourceRegistration& registration, std::ostream& output)
+ {
+ writeJsonLine(output, registration.resource->Schema());
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ResourceExecutor.h b/src/cascadia/TerminalDsc/Resource/ResourceExecutor.h
new file mode 100644
index 00000000000..a02fe1dab68
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ResourceExecutor.h
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "ResourceRegistry.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Runs one operation against one registered resource and writes the
+ // protocol output (compact JSON lines) to the stream. Capability presence
+ // is validated by the caller; these throw DscInputError / DscResourceError
+ // for input and state failures.
+ namespace ResourceExecutor
+ {
+ void ExecuteGet(const ResourceRegistration& registration, const std::optional& input, std::ostream& output);
+ void ExecuteSet(const ResourceRegistration& registration, const std::optional& input, std::ostream& output);
+ void ExecuteTest(const ResourceRegistration& registration, const std::optional& input, std::ostream& output);
+ void ExecuteDelete(const ResourceRegistration& registration, const std::optional& input, std::ostream& output);
+ void ExecuteExport(const ResourceRegistration& registration, const std::optional& input, std::ostream& output);
+ void ExecuteSchema(const ResourceRegistration& registration, std::ostream& output);
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ResourceMetadata.h b/src/cascadia/TerminalDsc/Resource/ResourceMetadata.h
new file mode 100644
index 00000000000..19ea3e9ceb7
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ResourceMetadata.h
@@ -0,0 +1,41 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+namespace Microsoft::Terminal::Dsc
+{
+ // What the "set" operation returns on stdout, mirrored into the manifest's
+ // "set.return" field.
+ enum class SetReturn
+ {
+ None,
+ State,
+ StateAndDiff,
+ };
+
+ // One documented process exit code, emitted into the manifest's "exitCodes" map.
+ struct ExitCodeInfo
+ {
+ int code;
+ std::string_view description;
+ };
+
+ // Static description of a Microsoft DSC resource, used for command routing
+ // and manifest generation.
+ struct ResourceMetadata
+ {
+ // Fully qualified resource type, e.g. "Microsoft.WindowsTerminal/Settings".
+ std::string_view type;
+ // Semantic version of the resource (independent of the app version).
+ std::string_view version;
+ std::string_view description;
+ std::vector tags;
+ SetReturn setReturn = SetReturn::State;
+ // Whether the manifest marks the get input as mandatory. Resources
+ // without key properties leave this false so a bare
+ // `dsc resource get` keeps working.
+ bool getRequiresInput = false;
+ std::vector exitCodes;
+ };
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ResourceRegistry.cpp b/src/cascadia/TerminalDsc/Resource/ResourceRegistry.cpp
new file mode 100644
index 00000000000..e4390e2a7dd
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ResourceRegistry.cpp
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "ResourceRegistry.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ namespace
+ {
+ bool equalsInsensitive(std::string_view left, std::string_view right) noexcept
+ {
+ return left.size() == right.size() &&
+ _strnicmp(left.data(), right.data(), left.size()) == 0;
+ }
+ }
+
+ void ResourceRegistry::_add(ResourceRegistration&& registration)
+ {
+ const auto type{ registration.Metadata().type };
+ if (Find(type))
+ {
+ throw std::invalid_argument{ fmt::format(FMT_COMPILE("resource '{}' is already registered"), type) };
+ }
+ _registrations.emplace_back(std::move(registration));
+ }
+
+ const ResourceRegistration* ResourceRegistry::Find(std::string_view type) const noexcept
+ {
+ for (const auto& registration : _registrations)
+ {
+ if (equalsInsensitive(registration.Metadata().type, type))
+ {
+ return ®istration;
+ }
+ }
+ return nullptr;
+ }
+}
diff --git a/src/cascadia/TerminalDsc/Resource/ResourceRegistry.h b/src/cascadia/TerminalDsc/Resource/ResourceRegistry.h
new file mode 100644
index 00000000000..f536e08b115
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resource/ResourceRegistry.h
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "DscResource.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Everything the dispatcher and the manifest generator need to know about
+ // one registered resource. The capability pointers are captured from the
+ // concrete type when the resource is added; a null pointer means the
+ // resource does not support that operation.
+ struct ResourceRegistration
+ {
+ std::unique_ptr resource;
+ IGettable* get = nullptr;
+ ISettable* set = nullptr;
+ ITestable* test = nullptr;
+ IDeletable* del = nullptr;
+ IExportable* exp = nullptr;
+
+ const ResourceMetadata& Metadata() const noexcept
+ {
+ return resource->Metadata();
+ }
+ };
+
+ // The registry of resources served by this executable, keyed on the fully
+ // qualified resource type (case-insensitive). Add new resources in
+ // main.cpp.
+ class ResourceRegistry
+ {
+ public:
+ // Registers a resource. Which capability interfaces the concrete type
+ // implements is determined at compile time; the repo
+ // builds without RTTI, so this cannot be a dynamic_cast. Throws
+ // std::invalid_argument when a resource with the same type is already
+ // registered.
+ template
+ void Add(std::unique_ptr resource)
+ {
+ static_assert(std::is_base_of_v, "resources must implement IDscResource");
+ ResourceRegistration registration;
+ if constexpr (std::is_base_of_v)
+ {
+ registration.get = resource.get();
+ }
+ if constexpr (std::is_base_of_v)
+ {
+ registration.set = resource.get();
+ }
+ if constexpr (std::is_base_of_v)
+ {
+ registration.test = resource.get();
+ }
+ if constexpr (std::is_base_of_v)
+ {
+ registration.del = resource.get();
+ }
+ if constexpr (std::is_base_of_v)
+ {
+ registration.exp = resource.get();
+ }
+ registration.resource = std::move(resource);
+ _add(std::move(registration));
+ }
+
+ // Case-insensitive lookup; returns nullptr when not found.
+ const ResourceRegistration* Find(std::string_view type) const noexcept;
+
+ size_t Count() const noexcept
+ {
+ return _registrations.size();
+ }
+
+ // When exactly one resource is registered, the --resource selector is
+ // implicit (and omitted from the generated manifest args).
+ bool IsSingleResource() const noexcept
+ {
+ return _registrations.size() == 1;
+ }
+
+ const std::vector& All() const noexcept
+ {
+ return _registrations;
+ }
+
+ private:
+ void _add(ResourceRegistration&& registration);
+
+ std::vector _registrations;
+ };
+}
diff --git a/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.cpp b/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.cpp
new file mode 100644
index 00000000000..51e39afaaed
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.cpp
@@ -0,0 +1,278 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "SettingsResource.h"
+
+using namespace Microsoft::Terminal::Dsc;
+
+namespace Model = winrt::Microsoft::Terminal::Settings::Model;
+
+namespace
+{
+ // One managed setting. To manage another global setting, add a row to
+ // settingProperties below and refresh the checked-in resource manifest
+ // (`TerminalDsc.exe manifest --save`).
+ struct SettingProperty
+ {
+ std::string_view jsonName;
+ Json::Value (*read)(const Model::GlobalAppSettings& globals);
+ void (*write)(const Model::GlobalAppSettings& globals, const Json::Value& value);
+ Json::Value (*schema)();
+ };
+
+ // "theme" round-trips the same two shapes settings.json stores: a single
+ // theme name, or a { "dark": ..., "light": ... } pair.
+ Json::Value readTheme(const Model::GlobalAppSettings& globals)
+ {
+ const auto theme{ globals.Theme() };
+ if (!theme)
+ {
+ return {};
+ }
+ const auto dark{ til::u16u8(theme.DarkName()) };
+ const auto light{ til::u16u8(theme.LightName()) };
+ if (dark == light)
+ {
+ return Json::Value{ dark };
+ }
+ Json::Value pair{ Json::objectValue };
+ pair["dark"] = dark;
+ pair["light"] = light;
+ return pair;
+ }
+
+ void writeTheme(const Model::GlobalAppSettings& globals, const Json::Value& value)
+ {
+ if (value.isString())
+ {
+ globals.Theme(Model::ThemePair{ winrt::hstring{ til::u8u16(value.asString()) } });
+ return;
+ }
+ if (value.isObject())
+ {
+ for (const auto& member : value.getMemberNames())
+ {
+ if (member != "dark" && member != "light")
+ {
+ throw DscInputError{ fmt::format(FMT_COMPILE("unknown 'theme' property '{}'; expected only 'dark' and 'light'"), member) };
+ }
+ }
+ const auto& dark{ value["dark"] };
+ const auto& light{ value["light"] };
+ if (!dark.isString() || !light.isString())
+ {
+ throw DscInputError{ "'theme' object requires 'dark' and 'light' strings" };
+ }
+ globals.Theme(Model::ThemePair{ winrt::hstring{ til::u8u16(dark.asString()) }, winrt::hstring{ til::u8u16(light.asString()) } });
+ return;
+ }
+ throw DscInputError{ "'theme' must be a theme name or an object with 'dark' and 'light' theme names" };
+ }
+
+ Json::Value themeSchema()
+ {
+ Json::Value stringForm{ Json::objectValue };
+ stringForm["type"] = "string";
+
+ Json::Value pairProperties{ Json::objectValue };
+ Json::Value themeName{ Json::objectValue };
+ themeName["type"] = "string";
+ pairProperties["dark"] = themeName;
+ pairProperties["light"] = themeName;
+
+ Json::Value pairForm{ Json::objectValue };
+ pairForm["type"] = "object";
+ pairForm["additionalProperties"] = false;
+ Json::Value required{ Json::arrayValue };
+ required.append("dark");
+ required.append("light");
+ pairForm["required"] = required;
+ pairForm["properties"] = pairProperties;
+
+ Json::Value schema{ Json::objectValue };
+ schema["description"] = "The theme of the application. Either the name of a built-in ('dark', 'light', 'system') or custom theme, or a pair of theme names applied when the OS is in dark or light mode.";
+ Json::Value oneOf{ Json::arrayValue };
+ oneOf.append(stringForm);
+ oneOf.append(pairForm);
+ schema["oneOf"] = oneOf;
+ return schema;
+ }
+
+ Json::Value readCopyOnSelect(const Model::GlobalAppSettings& globals)
+ {
+ return Json::Value{ globals.CopyOnSelect() };
+ }
+
+ void writeCopyOnSelect(const Model::GlobalAppSettings& globals, const Json::Value& value)
+ {
+ if (!value.isBool())
+ {
+ throw DscInputError{ "'copyOnSelect' must be a boolean" };
+ }
+ globals.CopyOnSelect(value.asBool());
+ }
+
+ Json::Value copyOnSelectSchema()
+ {
+ Json::Value schema{ Json::objectValue };
+ schema["description"] = "When true, a selection is immediately copied to the clipboard upon creation.";
+ schema["type"] = "boolean";
+ return schema;
+ }
+
+ Json::Value readInitialCols(const Model::GlobalAppSettings& globals)
+ {
+ return Json::Value{ globals.InitialCols() };
+ }
+
+ void writeInitialCols(const Model::GlobalAppSettings& globals, const Json::Value& value)
+ {
+ if (!value.isInt() || value.asInt() < 1 || value.asInt() > 999)
+ {
+ throw DscInputError{ "'initialCols' must be an integer between 1 and 999" };
+ }
+ globals.InitialCols(value.asInt());
+ }
+
+ Json::Value initialColsSchema()
+ {
+ Json::Value schema{ Json::objectValue };
+ schema["description"] = "The number of columns displayed in the window upon first load.";
+ schema["type"] = "integer";
+ schema["minimum"] = 1;
+ schema["maximum"] = 999;
+ return schema;
+ }
+
+ constexpr std::array settingProperties{
+ SettingProperty{ "theme", readTheme, writeTheme, themeSchema },
+ SettingProperty{ "copyOnSelect", readCopyOnSelect, writeCopyOnSelect, copyOnSelectSchema },
+ SettingProperty{ "initialCols", readInitialCols, writeInitialCols, initialColsSchema },
+ };
+
+ Model::CascadiaSettings loadSettings()
+ {
+ const auto settings{ Model::CascadiaSettings::LoadAll() };
+ // Refuse to touch a broken settings.json: applying state on top of the
+ // fallback defaults would overwrite whatever the user still has on disk.
+ if (settings.GetLoadingError())
+ {
+ throw DscResourceError{ "settings.json failed to load; launch Windows Terminal to see the error details" };
+ }
+ if (const auto errorMessage{ settings.GetSerializationErrorMessage() }; !errorMessage.empty())
+ {
+ throw DscResourceError{ fmt::format(FMT_COMPILE("settings.json failed to load: {}"), til::u16u8(errorMessage)) };
+ }
+ return settings;
+ }
+}
+
+const ResourceMetadata& SettingsResource::Metadata() const noexcept
+{
+ static const ResourceMetadata metadata{
+ .type = "Microsoft.WindowsTerminal/Settings",
+ .version = "0.1.0",
+ .description = "Manage Windows Terminal global settings.",
+ .tags = { "Windows", "WindowsTerminal", "Terminal" },
+ .setReturn = SetReturn::State,
+ .getRequiresInput = false,
+ .exitCodes = {
+ { ExitSuccess, "Success" },
+ { ExitError, "Error" },
+ { ExitInvalidInput, "Invalid desired state input" },
+ { ExitResourceError, "Failed to load or save the Windows Terminal settings file" },
+ },
+ };
+ return metadata;
+}
+
+Json::Value SettingsResource::ReadState(const Model::GlobalAppSettings& globals)
+{
+ Json::Value state{ Json::objectValue };
+ for (const auto& property : settingProperties)
+ {
+ auto value{ property.read(globals) };
+ if (!value.isNull())
+ {
+ state[std::string{ property.jsonName }] = std::move(value);
+ }
+ }
+ return state;
+}
+
+bool SettingsResource::ApplyState(const Model::GlobalAppSettings& globals, const Json::Value& desiredState)
+{
+ if (!desiredState.isObject())
+ {
+ throw DscInputError{ "the desired state must be a JSON object" };
+ }
+
+ for (const auto& member : desiredState.getMemberNames())
+ {
+ const auto known{ std::ranges::any_of(settingProperties, [&](const auto& property) { return property.jsonName == member; }) };
+ if (!known)
+ {
+ throw DscInputError{ fmt::format(FMT_COMPILE("unknown property '{}'; this resource manages: theme, copyOnSelect, initialCols"), member) };
+ }
+ }
+
+ auto changed{ false };
+ for (const auto& property : settingProperties)
+ {
+ const auto name{ std::string{ property.jsonName } };
+ if (desiredState.isMember(name))
+ {
+ property.write(globals, desiredState[name]);
+ changed = true;
+ }
+ }
+ return changed;
+}
+
+Json::Value SettingsResource::StateSchema()
+{
+ Json::Value schema{ Json::objectValue };
+ schema["$schema"] = "https://json-schema.org/draft/2020-12/schema";
+ schema["type"] = "object";
+ schema["additionalProperties"] = false;
+ Json::Value properties{ Json::objectValue };
+ for (const auto& property : settingProperties)
+ {
+ properties[std::string{ property.jsonName }] = property.schema();
+ }
+ schema["properties"] = properties;
+ return schema;
+}
+
+Json::Value SettingsResource::Schema() const
+{
+ return StateSchema();
+}
+
+Json::Value SettingsResource::Get(const std::optional& /*instance*/)
+{
+ // This resource has no key properties; any provided instance is ignored.
+ return ReadState(loadSettings().GlobalSettings());
+}
+
+Json::Value SettingsResource::Set(const Json::Value& instance)
+{
+ const auto settings{ loadSettings() };
+ const auto globals{ settings.GlobalSettings() };
+ if (ApplyState(globals, instance))
+ {
+ if (!settings.WriteSettingsToDisk())
+ {
+ throw DscResourceError{ "failed to write settings.json" };
+ }
+ }
+ return ReadState(globals);
+}
+
+std::vector SettingsResource::Export(const std::optional& /*filter*/)
+{
+ std::vector instances;
+ instances.emplace_back(Get(std::nullopt));
+ return instances;
+}
diff --git a/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.h b/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.h
new file mode 100644
index 00000000000..51d1bbb0315
--- /dev/null
+++ b/src/cascadia/TerminalDsc/Resources/Settings/SettingsResource.h
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "../../Resource/DscResource.h"
+
+namespace Microsoft::Terminal::Dsc
+{
+ // Manages Windows Terminal's global settings (the top-level scalars in
+ // settings.json). Microsoft DSC resource type: Microsoft.WindowsTerminal/Settings
+ class SettingsResource final : public IDscResource, public IGettable, public ISettable, public IExportable
+ {
+ public:
+ const ResourceMetadata& Metadata() const noexcept override;
+ Json::Value Schema() const override;
+
+ Json::Value Get(const std::optional& instance) override;
+ Json::Value Set(const Json::Value& instance) override;
+ std::vector Export(const std::optional& filter) override;
+
+ // The disk-free core of Get/Set/Schema, split out for unit testing:
+ // translate between the DSC property bag and a settings model instance.
+ static Json::Value ReadState(const winrt::Microsoft::Terminal::Settings::Model::GlobalAppSettings& globals);
+ static bool ApplyState(const winrt::Microsoft::Terminal::Settings::Model::GlobalAppSettings& globals, const Json::Value& desiredState);
+ static Json::Value StateSchema();
+ };
+}
diff --git a/src/cascadia/TerminalDsc/TerminalDsc.manifest b/src/cascadia/TerminalDsc/TerminalDsc.manifest
new file mode 100644
index 00000000000..83d15a9767b
--- /dev/null
+++ b/src/cascadia/TerminalDsc/TerminalDsc.manifest
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
diff --git a/src/cascadia/TerminalDsc/TerminalDsc.rc b/src/cascadia/TerminalDsc/TerminalDsc.rc
new file mode 100644
index 00000000000..e80117af7b9
--- /dev/null
+++ b/src/cascadia/TerminalDsc/TerminalDsc.rc
@@ -0,0 +1,68 @@
+// Microsoft Visual C++ generated resource script.
+//
+#include "resource.h"
+
+#define APSTUDIO_READONLY_SYMBOLS
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 2 resource.
+//
+#include "winres.h"
+/////////////////////////////////////////////////////////////////////////////
+#undef APSTUDIO_READONLY_SYMBOLS
+
+/////////////////////////////////////////////////////////////////////////////
+// English (United States) resources
+
+#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
+LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
+#pragma code_page(1252)
+
+#ifdef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// TEXTINCLUDE
+//
+
+1 TEXTINCLUDE
+BEGIN
+ "resource.h\0"
+END
+
+2 TEXTINCLUDE
+BEGIN
+ "#include ""winres.h""\0"
+END
+
+3 TEXTINCLUDE
+BEGIN
+ "\r\n"
+ "\0"
+END
+
+#endif // APSTUDIO_INVOKED
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+// Icon with lowest ID value placed first to ensure application icon
+// remains consistent on all systems.
+IDI_APPICON ICON "..\\..\\..\\res\\terminal.ico"
+
+#endif // English (United States) resources
+/////////////////////////////////////////////////////////////////////////////
+
+
+
+#ifndef APSTUDIO_INVOKED
+/////////////////////////////////////////////////////////////////////////////
+//
+// Generated from the TEXTINCLUDE 3 resource.
+//
+
+
+/////////////////////////////////////////////////////////////////////////////
+#endif // not APSTUDIO_INVOKED
diff --git a/src/cascadia/TerminalDsc/TerminalDsc.vcxproj b/src/cascadia/TerminalDsc/TerminalDsc.vcxproj
new file mode 100644
index 00000000000..f27ae030e9e
--- /dev/null
+++ b/src/cascadia/TerminalDsc/TerminalDsc.vcxproj
@@ -0,0 +1,157 @@
+
+
+
+
+ {CA5CAD1A-1C2D-46EF-B109-22320F070B04}
+ Win32Proj
+ TerminalDsc
+ TerminalDsc
+ TerminalDsc
+ Application
+ false
+ Windows Store
+ true
+ false
+ Windows
+ Windows Terminal DSC Resource
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+ true
+ $(OpenConsoleDir)\src\inc;$(OpenConsoleDir)\dep;$(OpenConsoleDir)\dep\gsl\include;%(AdditionalIncludeDirectories);
+
+
+ onecoreuap.lib;%(AdditionalDependencies)
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(OutDir)
+
+
+
+
+
+
+
+
+
+
+ {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}
+ false
+
+
+
+
+
+
+ $(OpenConsoleCommonOutDir)TerminalCore\Microsoft.Terminal.Core.winmd
+ true
+ false
+ false
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+ Console
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/TerminalDsc/main.cpp b/src/cascadia/TerminalDsc/main.cpp
new file mode 100644
index 00000000000..c2d4ecaed0b
--- /dev/null
+++ b/src/cascadia/TerminalDsc/main.cpp
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+#include "Resource/CommandBuilder.h"
+#include "Resource/ResourceRegistry.h"
+#include "Resources/Settings/SettingsResource.h"
+
+using namespace Microsoft::Terminal::Dsc;
+
+int wmain(int argc, wchar_t* argv[])
+{
+ winrt::init_apartment(winrt::apartment_type::multi_threaded);
+
+ // Register every Microsoft DSC resource this executable serves. To add a
+ // resource: implement IDscResource (+ capability interfaces) under
+ // Resources// and add it here
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+
+ CommandBuilder command{ std::move(registry) };
+
+ std::vector args;
+ args.reserve(argc > 1 ? gsl::narrow_cast(argc - 1) : 0);
+ for (auto i = 1; i < argc; ++i)
+ {
+ args.emplace_back(argv[i]);
+ }
+
+ return command.Run(args, std::cout);
+}
diff --git a/src/cascadia/TerminalDsc/microsoft.windowsterminal.settings.dsc.resource.json b/src/cascadia/TerminalDsc/microsoft.windowsterminal.settings.dsc.resource.json
new file mode 100644
index 00000000000..61203d16d84
--- /dev/null
+++ b/src/cascadia/TerminalDsc/microsoft.windowsterminal.settings.dsc.resource.json
@@ -0,0 +1,91 @@
+{
+ "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
+ "description": "Manage Windows Terminal global settings.",
+ "exitCodes": {
+ "0": "Success",
+ "1": "Error",
+ "2": "Invalid desired state input",
+ "3": "Failed to load or save the Windows Terminal settings file"
+ },
+ "export": {
+ "args": [
+ "export",
+ {
+ "jsonInputArg": "--input",
+ "mandatory": false
+ }
+ ],
+ "executable": "TerminalDsc"
+ },
+ "get": {
+ "args": [
+ "get",
+ {
+ "jsonInputArg": "--input",
+ "mandatory": false
+ }
+ ],
+ "executable": "TerminalDsc"
+ },
+ "schema": {
+ "embedded": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "copyOnSelect": {
+ "description": "When true, a selection is immediately copied to the clipboard upon creation.",
+ "type": "boolean"
+ },
+ "initialCols": {
+ "description": "The number of columns displayed in the window upon first load.",
+ "maximum": 999,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "theme": {
+ "description": "The theme of the application. Either the name of a built-in ('dark', 'light', 'system') or custom theme, or a pair of theme names applied when the OS is in dark or light mode.",
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dark": {
+ "type": "string"
+ },
+ "light": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "dark",
+ "light"
+ ],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ }
+ },
+ "set": {
+ "args": [
+ "set",
+ {
+ "jsonInputArg": "--input",
+ "mandatory": true
+ }
+ ],
+ "executable": "TerminalDsc",
+ "return": "state"
+ },
+ "tags": [
+ "Windows",
+ "WindowsTerminal",
+ "Terminal"
+ ],
+ "type": "Microsoft.WindowsTerminal/Settings",
+ "version": "0.1.0"
+}
diff --git a/src/cascadia/TerminalDsc/pch.cpp b/src/cascadia/TerminalDsc/pch.cpp
new file mode 100644
index 00000000000..93c90df0201
--- /dev/null
+++ b/src/cascadia/TerminalDsc/pch.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
diff --git a/src/cascadia/TerminalDsc/pch.h b/src/cascadia/TerminalDsc/pch.h
new file mode 100644
index 00000000000..8cd48c05525
--- /dev/null
+++ b/src/cascadia/TerminalDsc/pch.h
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#pragma once
+
+// Manually include til after we include Windows.Foundation to give it winrt superpowers
+#define BLOCK_TIL
+// This includes support libraries from the CRT, STL, WIL, and GSL
+#include
+
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+
+// Manually include til after we include Windows.Foundation to give it winrt superpowers
+#include "til.h"
+#include
+
+#include
diff --git a/src/cascadia/TerminalDsc/resource.h b/src/cascadia/TerminalDsc/resource.h
new file mode 100644
index 00000000000..b406a26f795
--- /dev/null
+++ b/src/cascadia/TerminalDsc/resource.h
@@ -0,0 +1,16 @@
+//{{NO_DEPENDENCIES}}
+// Microsoft Visual C++ generated include file.
+// Used by TerminalDsc.rc
+//
+#define IDI_APPICON 101
+
+// Next default values for new objects
+//
+#ifdef APSTUDIO_INVOKED
+#ifndef APSTUDIO_READONLY_SYMBOLS
+#define _APS_NEXT_RESOURCE_VALUE 102
+#define _APS_NEXT_COMMAND_VALUE 40001
+#define _APS_NEXT_CONTROL_VALUE 1001
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/src/cascadia/UnitTests_TerminalDsc/ManifestGeneratorTests.cpp b/src/cascadia/UnitTests_TerminalDsc/ManifestGeneratorTests.cpp
new file mode 100644
index 00000000000..0e766ad5d8e
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/ManifestGeneratorTests.cpp
@@ -0,0 +1,161 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "../TerminalDsc/Resource/JsonUtils.h"
+#include "../TerminalDsc/Resource/ManifestGenerator.h"
+#include "../TerminalDsc/Resources/Settings/SettingsResource.h"
+
+using namespace Microsoft::Terminal::Dsc;
+using namespace WEX::Logging;
+using namespace WEX::TestExecution;
+using namespace WEX::Common;
+
+namespace TerminalDscUnitTests
+{
+ namespace
+ {
+ // A second, minimal resource to exercise the multi-resource shapes.
+ struct FakeResource final : IDscResource, IGettable
+ {
+ const ResourceMetadata& Metadata() const noexcept override
+ {
+ static const ResourceMetadata metadata{
+ .type = "Test/Fake",
+ .version = "1.0.0",
+ .description = "A fake resource for tests.",
+ .setReturn = SetReturn::None,
+ };
+ return metadata;
+ }
+
+ Json::Value Schema() const override
+ {
+ Json::Value schema{ Json::objectValue };
+ schema["type"] = "object";
+ return schema;
+ }
+
+ Json::Value Get(const std::optional& /*instance*/) override
+ {
+ return Json::Value{ Json::objectValue };
+ }
+ };
+ }
+
+ class ManifestGeneratorTests
+ {
+ TEST_CLASS(ManifestGeneratorTests);
+
+ TEST_METHOD(GeneratedManifestMatchesCheckedInFile);
+ TEST_METHOD(MultiResourceDocumentAndFileNames);
+ };
+
+ void ManifestGeneratorTests::GeneratedManifestMatchesCheckedInFile()
+ {
+ static constexpr std::string_view expectedManifest{ R"({
+ "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
+ "description": "Manage Windows Terminal global settings.",
+ "exitCodes": {
+ "0": "Success",
+ "1": "Error",
+ "2": "Invalid desired state input",
+ "3": "Failed to load or save the Windows Terminal settings file"
+ },
+ "export": {
+ "args": ["export", { "jsonInputArg": "--input", "mandatory": false }],
+ "executable": "TerminalDsc"
+ },
+ "get": {
+ "args": ["get", { "jsonInputArg": "--input", "mandatory": false }],
+ "executable": "TerminalDsc"
+ },
+ "schema": {
+ "embedded": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "additionalProperties": false,
+ "properties": {
+ "copyOnSelect": {
+ "description": "When true, a selection is immediately copied to the clipboard upon creation.",
+ "type": "boolean"
+ },
+ "initialCols": {
+ "description": "The number of columns displayed in the window upon first load.",
+ "maximum": 999,
+ "minimum": 1,
+ "type": "integer"
+ },
+ "theme": {
+ "description": "The theme of the application. Either the name of a built-in ('dark', 'light', 'system') or custom theme, or a pair of theme names applied when the OS is in dark or light mode.",
+ "oneOf": [
+ { "type": "string" },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "dark": { "type": "string" },
+ "light": { "type": "string" }
+ },
+ "required": ["dark", "light"],
+ "type": "object"
+ }
+ ]
+ }
+ },
+ "type": "object"
+ }
+ },
+ "set": {
+ "args": ["set", { "jsonInputArg": "--input", "mandatory": true }],
+ "executable": "TerminalDsc",
+ "return": "state"
+ },
+ "tags": ["Windows", "WindowsTerminal", "Terminal"],
+ "type": "Microsoft.WindowsTerminal/Settings",
+ "version": "0.1.0"
+ })" };
+
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+
+ const auto document{ ManifestGenerator::BuildManifestDocument(registry, "TerminalDsc") };
+
+ // Compare serialized forms: jsoncpp stores object members sorted, so
+ // this is insensitive to authoring order but pins names and values.
+ VERIFY_ARE_EQUAL(SerializeJson(ParseJson(expectedManifest)), SerializeJson(document));
+
+ VERIFY_ARE_EQUAL(std::string{ "microsoft.windowsterminal.settings.dsc.resource.json" },
+ ManifestGenerator::ManifestFileName(registry, "TerminalDsc"));
+ }
+
+ void ManifestGeneratorTests::MultiResourceDocumentAndFileNames()
+ {
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+ registry.Add(std::make_unique());
+
+ const auto document{ ManifestGenerator::BuildManifestDocument(registry, "TerminalDsc") };
+
+ // With two or more resources the document becomes a manifest list and
+ // every operation's args carry the --resource selector.
+ VERIFY_IS_TRUE(document.isMember("resources"));
+ VERIFY_ARE_EQUAL(2u, document["resources"].size());
+
+ const auto& first{ document["resources"][0] };
+ const auto& args{ first["get"]["args"] };
+ VERIFY_ARE_EQUAL(std::string{ "get" }, args[0].asString());
+ VERIFY_ARE_EQUAL(std::string{ "--resource" }, args[1].asString());
+ VERIFY_ARE_EQUAL(std::string{ "Microsoft.WindowsTerminal/Settings" }, args[2].asString());
+
+ // The fake resource is get-only: no set/export sections.
+ const auto& second{ document["resources"][1] };
+ VERIFY_IS_TRUE(second.isMember("get"));
+ VERIFY_IS_FALSE(second.isMember("set"));
+ VERIFY_IS_FALSE(second.isMember("export"));
+ VERIFY_IS_FALSE(second.isMember("tags"));
+ VERIFY_IS_FALSE(second.isMember("exitCodes"));
+
+ VERIFY_ARE_EQUAL(std::string{ "TerminalDsc.dsc.manifests.json" },
+ ManifestGenerator::ManifestFileName(registry, "TerminalDsc"));
+ }
+}
diff --git a/src/cascadia/UnitTests_TerminalDsc/ResourceRegistryTests.cpp b/src/cascadia/UnitTests_TerminalDsc/ResourceRegistryTests.cpp
new file mode 100644
index 00000000000..9cf5576c884
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/ResourceRegistryTests.cpp
@@ -0,0 +1,67 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "../TerminalDsc/Resource/ResourceRegistry.h"
+#include "../TerminalDsc/Resources/Settings/SettingsResource.h"
+
+using namespace Microsoft::Terminal::Dsc;
+using namespace WEX::Logging;
+using namespace WEX::TestExecution;
+using namespace WEX::Common;
+
+namespace TerminalDscUnitTests
+{
+ class ResourceRegistryTests
+ {
+ TEST_CLASS(ResourceRegistryTests);
+
+ TEST_METHOD(RegistrationCapturesCapabilities);
+ TEST_METHOD(FindIsCaseInsensitive);
+ TEST_METHOD(DuplicateRegistrationThrows);
+ TEST_METHOD(SingleResourceDetection);
+ };
+
+ void ResourceRegistryTests::RegistrationCapturesCapabilities()
+ {
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+
+ const auto registration{ registry.Find("Microsoft.WindowsTerminal/Settings") };
+ VERIFY_IS_NOT_NULL(registration);
+ VERIFY_IS_NOT_NULL(registration->get);
+ VERIFY_IS_NOT_NULL(registration->set);
+ VERIFY_IS_NOT_NULL(registration->exp);
+ VERIFY_IS_NULL(registration->test);
+ VERIFY_IS_NULL(registration->del);
+ }
+
+ void ResourceRegistryTests::FindIsCaseInsensitive()
+ {
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+
+ VERIFY_IS_NOT_NULL(registry.Find("microsoft.windowsterminal/settings"));
+ VERIFY_IS_NOT_NULL(registry.Find("MICROSOFT.WINDOWSTERMINAL/SETTINGS"));
+ VERIFY_IS_NULL(registry.Find("Microsoft.WindowsTerminal/DoesNotExist"));
+ }
+
+ void ResourceRegistryTests::DuplicateRegistrationThrows()
+ {
+ ResourceRegistry registry;
+ registry.Add(std::make_unique());
+
+ VERIFY_THROWS_SPECIFIC(registry.Add(std::make_unique()), const std::invalid_argument, [](const auto&) { return true; });
+ }
+
+ void ResourceRegistryTests::SingleResourceDetection()
+ {
+ ResourceRegistry registry;
+ VERIFY_IS_FALSE(registry.IsSingleResource());
+
+ registry.Add(std::make_unique());
+ VERIFY_IS_TRUE(registry.IsSingleResource());
+ VERIFY_ARE_EQUAL(static_cast(1), registry.Count());
+ }
+}
diff --git a/src/cascadia/UnitTests_TerminalDsc/SettingsResourceTests.cpp b/src/cascadia/UnitTests_TerminalDsc/SettingsResourceTests.cpp
new file mode 100644
index 00000000000..856c20831b2
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/SettingsResourceTests.cpp
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
+
+#include "../TerminalSettingsModel/CascadiaSettings.h"
+#include "../TerminalDsc/Resource/JsonUtils.h"
+#include "../TerminalDsc/Resources/Settings/SettingsResource.h"
+
+using namespace Microsoft::Console;
+using namespace Microsoft::Terminal::Dsc;
+using namespace WEX::Logging;
+using namespace WEX::TestExecution;
+using namespace WEX::Common;
+using namespace winrt::Microsoft::Terminal::Settings::Model;
+using namespace winrt::Microsoft::Terminal::Settings;
+
+namespace TerminalDscUnitTests
+{
+ class SettingsResourceTests
+ {
+ TEST_CLASS(SettingsResourceTests);
+
+ TEST_METHOD(ReadStateReportsConfiguredValues);
+ TEST_METHOD(ReadStateReportsThemePair);
+ TEST_METHOD(ReadStateReportsDefaultTheme);
+ TEST_METHOD(ApplyStateUpdatesSettings);
+ TEST_METHOD(ApplyStateWithEmptyObjectChangesNothing);
+ TEST_METHOD(ApplyStateRejectsUnknownProperties);
+ TEST_METHOD(ApplyStateRejectsWrongTypes);
+ TEST_METHOD(SerializedJsonIsASingleLine);
+ TEST_METHOD(ParseJsonRejectsMalformedInput);
+
+ private:
+ // The settings loader rejects a document without profiles, so wrap the
+ // globals under test in a minimal valid settings document.
+ static winrt::com_ptr createSettings(const std::string_view& globalsJSON)
+ {
+ std::string json{ R"({ "profiles": [ { "name": "profile0" } ])" };
+ if (!globalsJSON.empty())
+ {
+ json += ", ";
+ json += globalsJSON;
+ }
+ json += " }";
+ return winrt::make_self(json);
+ }
+ };
+
+ void SettingsResourceTests::ReadStateReportsConfiguredValues()
+ {
+ // The referenced theme must exist, or settings validation resets the
+ // setting to "system" (the built-in themes live in the defaults.json
+ // layer, which these unit tests do not load). User themes with the
+ // reserved built-in names are ignored, so use a distinct name.
+ static constexpr std::string_view userSettings{ R"(
+ "themes": [ { "name": "testLight" } ],
+ "theme": "testLight",
+ "copyOnSelect": true,
+ "initialCols": 42
+ )" };
+
+ const auto settings{ createSettings(userSettings) };
+ const auto state{ SettingsResource::ReadState(settings->GlobalSettings()) };
+
+ VERIFY_ARE_EQUAL(std::string{ "testLight" }, state["theme"].asString());
+ VERIFY_IS_TRUE(state["copyOnSelect"].asBool());
+ VERIFY_ARE_EQUAL(42, state["initialCols"].asInt());
+ }
+
+ void SettingsResourceTests::ReadStateReportsThemePair()
+ {
+ static constexpr std::string_view userSettings{ R"(
+ "themes": [ { "name": "testDark" }, { "name": "testLight" } ],
+ "theme": { "dark": "testDark", "light": "testLight" }
+ )" };
+
+ const auto settings{ createSettings(userSettings) };
+ const auto state{ SettingsResource::ReadState(settings->GlobalSettings()) };
+
+ VERIFY_IS_TRUE(state["theme"].isObject());
+ VERIFY_ARE_EQUAL(std::string{ "testDark" }, state["theme"]["dark"].asString());
+ VERIFY_ARE_EQUAL(std::string{ "testLight" }, state["theme"]["light"].asString());
+ }
+
+ void SettingsResourceTests::ReadStateReportsDefaultTheme()
+ {
+ const auto settings{ createSettings("") };
+ const auto state{ SettingsResource::ReadState(settings->GlobalSettings()) };
+
+ // When no theme is configured, settings validation falls back to
+ // "system"; the scalar settings always have a value.
+ VERIFY_ARE_EQUAL(std::string{ "system" }, state["theme"].asString());
+ VERIFY_IS_TRUE(state.isMember("copyOnSelect"));
+ VERIFY_IS_TRUE(state.isMember("initialCols"));
+ }
+
+ void SettingsResourceTests::ApplyStateUpdatesSettings()
+ {
+ const auto settings{ createSettings("") };
+ const auto globals{ settings->GlobalSettings() };
+
+ const auto desired{ ParseJson(R"({ "copyOnSelect": true, "initialCols": 200 })") };
+ VERIFY_IS_TRUE(SettingsResource::ApplyState(globals, desired));
+
+ VERIFY_IS_TRUE(globals.CopyOnSelect());
+ VERIFY_ARE_EQUAL(200, globals.InitialCols());
+
+ const auto state{ SettingsResource::ReadState(globals) };
+ VERIFY_IS_TRUE(state["copyOnSelect"].asBool());
+ VERIFY_ARE_EQUAL(200, state["initialCols"].asInt());
+ }
+
+ void SettingsResourceTests::ApplyStateWithEmptyObjectChangesNothing()
+ {
+ const auto settings{ createSettings("") };
+ VERIFY_IS_FALSE(SettingsResource::ApplyState(settings->GlobalSettings(), ParseJson("{}")));
+ }
+
+ void SettingsResourceTests::ApplyStateRejectsUnknownProperties()
+ {
+ const auto settings{ createSettings("") };
+ const auto globals{ settings->GlobalSettings() };
+
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "initialRows": 5 })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"([ "not", "an", "object" ])")), const DscInputError, [](const auto&) { return true; });
+ }
+
+ void SettingsResourceTests::ApplyStateRejectsWrongTypes()
+ {
+ const auto settings{ createSettings("") };
+ const auto globals{ settings->GlobalSettings() };
+
+ // None of these throw until after validation, so the settings object is never touched.
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "copyOnSelect": "yes" })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "initialCols": 0 })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "initialCols": 1000 })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "initialCols": true })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "theme": 42 })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "theme": { "dark": "dark" } })")), const DscInputError, [](const auto&) { return true; });
+ VERIFY_THROWS_SPECIFIC(SettingsResource::ApplyState(globals, ParseJson(R"({ "theme": { "dark": "dark", "light": "light", "other": "x" } })")), const DscInputError, [](const auto&) { return true; });
+ }
+
+ void SettingsResourceTests::SerializedJsonIsASingleLine()
+ {
+ const auto text{ SerializeJson(SettingsResource::StateSchema()) };
+ VERIFY_ARE_EQUAL(std::string::npos, text.find('\n'));
+ }
+
+ void SettingsResourceTests::ParseJsonRejectsMalformedInput()
+ {
+ VERIFY_THROWS_SPECIFIC(ParseJson("{ not json"), const DscInputError, [](const auto&) { return true; });
+ }
+}
diff --git a/src/cascadia/UnitTests_TerminalDsc/TerminalDsc.UnitTests.vcxproj b/src/cascadia/UnitTests_TerminalDsc/TerminalDsc.UnitTests.vcxproj
new file mode 100644
index 00000000000..4bf35f5b42c
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/TerminalDsc.UnitTests.vcxproj
@@ -0,0 +1,98 @@
+
+
+
+ {CA5CAD1A-C663-4A5D-99D4-D7080F26C861}
+ Win32Proj
+ TerminalDscUnitTests
+ UnitTests_TerminalDsc
+ TerminalDsc.Unit.Tests
+ DynamicLibrary
+ true
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {CA5CAD1A-039A-4929-BA2A-8BEB2E4106FE}
+ false
+
+
+
+
+
+
+ ..;$(OpenConsoleDir)\dep;$(OpenConsoleDir)src\inc;$(OpenConsoleDir)src\inc\test;$(WinRT_IncludePath)\..\cppwinrt\winrt;"$(OpenConsoleDir)\src\cascadia\TerminalSettingsModel\Generated Files";%(AdditionalIncludeDirectories)
+ pch.h
+
+ 4702;%(DisableSpecificWarnings)
+
+
+ onecoreuap.lib;%(AdditionalDependencies);$(IntDir)\..\Microsoft.Terminal.Settings.Model.lib\Microsoft.Terminal.Settings.Model.res
+
+ %(AdditionalOptions) /INCLUDE:_DllMain@12
+ %(AdditionalOptions) /INCLUDE:DllMain
+
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/cascadia/UnitTests_TerminalDsc/pch.cpp b/src/cascadia/UnitTests_TerminalDsc/pch.cpp
new file mode 100644
index 00000000000..93c90df0201
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/pch.cpp
@@ -0,0 +1,4 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT license.
+
+#include "pch.h"
diff --git a/src/cascadia/UnitTests_TerminalDsc/pch.h b/src/cascadia/UnitTests_TerminalDsc/pch.h
new file mode 100644
index 00000000000..05e713a0ab2
--- /dev/null
+++ b/src/cascadia/UnitTests_TerminalDsc/pch.h
@@ -0,0 +1,77 @@
+/*++
+Copyright (c) Microsoft Corporation
+Licensed under the MIT license.
+
+Module Name:
+- pch.h
+
+Abstract:
+- Contains external headers to include in the precompile phase of console build process.
+- Avoid including internal project headers. Instead include them only in the classes that need them (helps with test project building).
+--*/
+
+#pragma once
+
+// Manually include til after we include Windows.Foundation to give it winrt superpowers
+#define BLOCK_TIL
+// This includes support libraries from the CRT, STL, WIL, and GSL
+#include
+// This is inexplicable, but for whatever reason, cppwinrt conflicts with the
+// SDK definition of this function, so the only fix is to undef it.
+// from WinBase.h
+// Windows::UI::Xaml::Media::Animation::IStoryboard::GetCurrentTime
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include "consoletaeftemplates.hpp"
+#include "winrtTaefTemplates.hpp"
+
+#include
+#include "winrt/Windows.UI.Xaml.Markup.h"
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+// Manually include til after we include Windows.Foundation to give it winrt superpowers
+#include "til.h"
+#include
+#include
+#include
+#include
+
+// Common includes for most tests:
+#include "../../inc/conattrs.hpp"
+#include "../../types/inc/utils.hpp"
+#include "../../inc/DefaultSettings.h"
+
+#include
+
+#include
diff --git a/tools/tests.xml b/tools/tests.xml
index 7150fbfcde5..bf96441bc07 100644
--- a/tools/tests.xml
+++ b/tools/tests.xml
@@ -6,6 +6,7 @@
+