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 @@ +