diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt
index f063332bb8..8c2cff836e 100644
--- a/.github/actions/spelling/expect.txt
+++ b/.github/actions/spelling/expect.txt
@@ -143,6 +143,7 @@ DUPLICATEALIAS
dustojnikhummer
dvinns
dwgs
+dwrite
ecfr
ecfrbrowse
EFGH
diff --git a/schemas/JSON/settings/settings.schema.0.2.json b/schemas/JSON/settings/settings.schema.0.2.json
index 7cc2001c1d..2113127f0e 100644
--- a/schemas/JSON/settings/settings.schema.0.2.json
+++ b/schemas/JSON/settings/settings.schema.0.2.json
@@ -287,6 +287,11 @@
"description": "Enable support for the configure export command",
"type": "boolean",
"default": false
+ },
+ "fonts": {
+ "description": "Enable support for managing fonts",
+ "type": "boolean",
+ "default": false
}
}
}
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
index 58c83d1efb..45c9c0fa04 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj
@@ -364,6 +364,7 @@
+
@@ -410,6 +411,7 @@
+
@@ -441,6 +443,7 @@
+
@@ -490,6 +493,7 @@
+
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
index 6c072ed000..a8f48edd34 100644
--- a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
+++ b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters
@@ -260,6 +260,12 @@
Header Files
+
+ Commands
+
+
+ Workflows
+
@@ -490,6 +496,12 @@
Source Files
+
+ Commands
+
+
+ Workflows
+
diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp
index a5334a2ceb..0b1864df3c 100644
--- a/src/AppInstallerCLICore/Argument.cpp
+++ b/src/AppInstallerCLICore/Argument.cpp
@@ -198,6 +198,10 @@ namespace AppInstaller::CLI
case Execution::Args::Type::IgnoreResumeLimit:
return { type, "ignore-resume-limit"_liv, ArgTypeCategory::None };
+ // Font command
+ case Execution::Args::Type::Family:
+ return { type, "family"_liv, ArgTypeCategory::None };
+
// Configuration commands
case Execution::Args::Type::ConfigurationFile:
return { type, "file"_liv, 'f', ArgTypeCategory::ConfigurationSetChoice, ArgTypeExclusiveSet::ConfigurationSetChoice };
@@ -430,6 +434,8 @@ namespace AppInstaller::CLI
return Argument{ type, Resource::String::ProxyArgumentDescription, ArgumentType::Standard, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions };
case Args::Type::NoProxy:
return Argument{ type, Resource::String::NoProxyArgumentDescription, ArgumentType::Flag, TogglePolicy::Policy::ProxyCommandLineOptions, BoolAdminSetting::ProxyCommandLineOptions };
+ case Args::Type::Family:
+ return Argument{ type, Resource::String::FontFamilyNameArgumentDescription, ArgumentType::Positional, false };
default:
THROW_HR(E_UNEXPECTED);
}
diff --git a/src/AppInstallerCLICore/Commands/FontCommand.cpp b/src/AppInstallerCLICore/Commands/FontCommand.cpp
new file mode 100644
index 0000000000..c6faae5b44
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/FontCommand.cpp
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "FontCommand.h"
+#include "Workflows/CompletionFlow.h"
+#include "Workflows/WorkflowBase.h"
+#include "Workflows/FontFlow.h"
+#include "Resources.h"
+
+namespace AppInstaller::CLI
+{
+ using namespace AppInstaller::CLI::Execution;
+ using namespace AppInstaller::CLI::Workflow;
+ using namespace AppInstaller::Utility::literals;
+ using namespace std::string_view_literals;
+
+ Utility::LocIndView s_FontCommand_HelpLink = "https://aka.ms/winget-command-font"_liv;
+
+ std::vector> FontCommand::GetCommands() const
+ {
+ return InitializeFromMoveOnly>>({
+ std::make_unique(FullName()),
+ });
+ }
+
+ Resource::LocString FontCommand::ShortDescription() const
+ {
+ return { Resource::String::FontCommandShortDescription };
+ }
+
+ Resource::LocString FontCommand::LongDescription() const
+ {
+ return { Resource::String::FontCommandLongDescription };
+ }
+
+ Utility::LocIndView FontCommand::HelpLink() const
+ {
+ return s_FontCommand_HelpLink;
+ }
+
+ void FontCommand::ExecuteInternal(Execution::Context& context) const
+ {
+ OutputHelp(context.Reporter);
+ }
+
+ std::vector FontListCommand::GetArguments() const
+ {
+ return {
+ Argument::ForType(Args::Type::Family),
+ Argument::ForType(Args::Type::Moniker),
+ Argument::ForType(Args::Type::Source),
+ Argument::ForType(Args::Type::Tag),
+ Argument::ForType(Args::Type::Exact),
+ Argument::ForType(Args::Type::AuthenticationMode),
+ Argument::ForType(Args::Type::AuthenticationAccount),
+ Argument::ForType(Args::Type::AcceptSourceAgreements),
+ };
+ }
+
+ Resource::LocString FontListCommand::ShortDescription() const
+ {
+ return { Resource::String::FontListCommandShortDescription };
+ }
+
+ Resource::LocString FontListCommand::LongDescription() const
+ {
+ return { Resource::String::FontListCommandLongDescription };
+ }
+
+ void FontListCommand::Complete(Execution::Context& context, Args::Type valueType) const
+ {
+ UNREFERENCED_PARAMETER(valueType);
+ context.Reporter.Error() << Resource::String::PendingWorkError << std::endl;
+ THROW_HR(E_NOTIMPL);
+ }
+
+ Utility::LocIndView FontListCommand::HelpLink() const
+ {
+ return s_FontCommand_HelpLink;
+ }
+
+ void FontListCommand::ExecuteInternal(Execution::Context& context) const
+ {
+ context << Workflow::ReportInstalledFonts;
+ }
+}
diff --git a/src/AppInstallerCLICore/Commands/FontCommand.h b/src/AppInstallerCLICore/Commands/FontCommand.h
new file mode 100644
index 0000000000..c29f703f6c
--- /dev/null
+++ b/src/AppInstallerCLICore/Commands/FontCommand.h
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "Command.h"
+#include
+
+namespace AppInstaller::CLI
+{
+ struct FontCommand final : public Command
+ {
+ FontCommand(std::string_view parent) : Command("font", { "fonts" }, parent, Settings::ExperimentalFeature::Feature::Font) {}
+
+ std::vector> GetCommands() const override;
+
+ Resource::LocString ShortDescription() const override;
+ Resource::LocString LongDescription() const override;
+
+ Utility::LocIndView HelpLink() const override;
+
+ protected:
+ void ExecuteInternal(Execution::Context& context) const override;
+ };
+
+ struct FontListCommand final : public Command
+ {
+ FontListCommand(std::string_view parent) : Command("list", parent) {}
+
+ std::vector GetArguments() const override;
+
+ Resource::LocString ShortDescription() const override;
+ Resource::LocString LongDescription() const override;
+
+ void Complete(Execution::Context& context, Execution::Args::Type valueType) const override;
+
+ Utility::LocIndView HelpLink() const override;
+
+ protected:
+ void ExecuteInternal(Execution::Context& context) const override;
+ };
+}
diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp
index f5a61d79fa..5580efa14a 100644
--- a/src/AppInstallerCLICore/Commands/RootCommand.cpp
+++ b/src/AppInstallerCLICore/Commands/RootCommand.cpp
@@ -15,6 +15,7 @@
#include "ValidateCommand.h"
#include "SettingsCommand.h"
#include "FeaturesCommand.h"
+#include "FontCommand.h"
#include "ExperimentalCommand.h"
#include "CompleteCommand.h"
#include "ExportCommand.h"
@@ -194,6 +195,7 @@ namespace AppInstaller::CLI
std::make_unique(FullName()),
std::make_unique(FullName()),
std::make_unique(FullName()),
+ std::make_unique(FullName()),
#if _DEBUG
std::make_unique(FullName()),
#endif
diff --git a/src/AppInstallerCLICore/ExecutionArgs.h b/src/AppInstallerCLICore/ExecutionArgs.h
index 41739b36f4..7bb2c28628 100644
--- a/src/AppInstallerCLICore/ExecutionArgs.h
+++ b/src/AppInstallerCLICore/ExecutionArgs.h
@@ -121,6 +121,9 @@ namespace AppInstaller::CLI::Execution
ResumeId,
IgnoreResumeLimit,
+ // Font Command
+ Family,
+
// Configuration
ConfigurationFile,
ConfigurationAcceptWarning,
diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h
index 0e8407ab2e..5aa7616509 100644
--- a/src/AppInstallerCLICore/Resources.h
+++ b/src/AppInstallerCLICore/Resources.h
@@ -248,6 +248,16 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(FileNotFound);
WINGET_DEFINE_RESOURCE_STRINGID(FilesRemainInInstallDirectory);
WINGET_DEFINE_RESOURCE_STRINGID(FlagContainAdjoinedError);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontCommandLongDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFace);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFaces);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFamily);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFamilyNameArgumentDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontFilePaths);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandLongDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontListCommandShortDescription);
+ WINGET_DEFINE_RESOURCE_STRINGID(FontVersion);
WINGET_DEFINE_RESOURCE_STRINGID(ForceArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(GatedVersionArgumentDescription);
WINGET_DEFINE_RESOURCE_STRINGID(GetManifestResultVersionNotFound);
@@ -403,6 +413,7 @@ namespace AppInstaller::CLI::Resource
WINGET_DEFINE_RESOURCE_STRINGID(NoAdminRepairForUserScopePackage);
WINGET_DEFINE_RESOURCE_STRINGID(NoApplicableInstallers);
WINGET_DEFINE_RESOURCE_STRINGID(NoExperimentalFeaturesMessage);
+ WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledFontFound);
WINGET_DEFINE_RESOURCE_STRINGID(NoInstalledPackageFound);
WINGET_DEFINE_RESOURCE_STRINGID(NoPackageFound);
WINGET_DEFINE_RESOURCE_STRINGID(NoPackageSelectionArgumentProvided);
diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.cpp b/src/AppInstallerCLICore/Workflows/FontFlow.cpp
new file mode 100644
index 0000000000..ce9bb7a233
--- /dev/null
+++ b/src/AppInstallerCLICore/Workflows/FontFlow.cpp
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "FontFlow.h"
+#include "TableOutput.h"
+#include
+#include
+
+namespace AppInstaller::CLI::Workflow
+{
+ using namespace AppInstaller::CLI::Execution;
+
+ namespace
+ {
+ struct InstalledFontFamiliesTableLine
+ {
+ InstalledFontFamiliesTableLine(Utility::LocIndString familyName, int faceCount)
+ : FamilyName(familyName), FaceCount(faceCount) {}
+
+ Utility::LocIndString FamilyName;
+ int FaceCount;
+ };
+
+ struct InstalledFontFacesTableLine
+ {
+ InstalledFontFacesTableLine(Utility::LocIndString familyName, Utility::LocIndString faceName, Utility::LocIndString faceVersion, std::filesystem::path filePath)
+ : FamilyName(familyName), FaceName(faceName), FaceVersion(faceVersion), FilePath(filePath) {}
+
+ Utility::LocIndString FamilyName;
+ Utility::LocIndString FaceName;
+ Utility::LocIndString FaceVersion;
+ std::filesystem::path FilePath;
+ };
+
+ void OutputInstalledFontFamiliesTable(Execution::Context& context, const std::vector& lines)
+ {
+ Execution::TableOutput<2> table(context.Reporter, { Resource::String::FontFamily, Resource::String::FontFaces });
+
+ for (auto line : lines)
+ {
+ table.OutputLine({ line.FamilyName, std::to_string(line.FaceCount) });
+ }
+
+ table.Complete();
+ }
+
+ void OutputInstalledFontFacesTable(Execution::Context& context, const std::vector& lines)
+ {
+ Execution::TableOutput<4> table(context.Reporter, { Resource::String::FontFamily, Resource::String::FontFace, Resource::String::FontVersion, Resource::String::FontFilePaths });
+
+ bool anonymizePath = Settings::User().Get();
+
+ for (auto line : lines)
+ {
+ if (anonymizePath)
+ {
+ AppInstaller::Runtime::ReplaceProfilePathsWithEnvironmentVariable(line.FilePath);
+ }
+
+ table.OutputLine({ line.FamilyName, line.FaceName, line.FaceVersion, line.FilePath.u8string() });
+ }
+
+ table.Complete();
+ }
+ }
+
+ void ReportInstalledFonts(Execution::Context& context)
+ {
+ Fonts::FontCatalog fontCatalog;
+
+ if (context.Args.Contains(Args::Type::Family))
+ {
+ // TODO: Create custom source and search mechanism for fonts.
+ const auto& familyNameArg = AppInstaller::Utility::ConvertToUTF16(context.Args.GetArg(Args::Type::Family));
+ const auto& fontFamilies = fontCatalog.GetInstalledFontFamilies(familyNameArg);
+
+ if (fontFamilies.empty())
+ {
+ context.Reporter.Info() << Resource::String::NoInstalledFontFound << std::endl;
+ return;
+ }
+
+ std::vector lines;
+
+ for (const auto& fontFamily : fontFamilies)
+ {
+ const auto& familyName = Utility::LocIndString(Utility::ConvertToUTF8(fontFamily.Name));
+
+ for (const auto& fontFace : fontFamily.Faces)
+ {
+ for (const auto& filePath : fontFace.FilePaths)
+ {
+ InstalledFontFacesTableLine line(
+ familyName,
+ Utility::LocIndString(Utility::ToLower(Utility::ConvertToUTF8(fontFace.Name))),
+ Utility::LocIndString(fontFace.Version.ToString()),
+ filePath.u8string());
+
+ lines.push_back(std::move(line));
+ }
+ }
+ }
+
+ OutputInstalledFontFacesTable(context, lines);
+ }
+ else
+ {
+ const auto& fontFamilies = fontCatalog.GetInstalledFontFamilies();
+ std::vector lines;
+
+ for (const auto& fontFamily : fontFamilies)
+ {
+ InstalledFontFamiliesTableLine line(
+ Utility::LocIndString(Utility::ConvertToUTF8(fontFamily.Name)),
+ static_cast(fontFamily.Faces.size())
+ );
+
+ lines.push_back(std::move(line));
+ }
+
+ OutputInstalledFontFamiliesTable(context, lines);
+ }
+ }
+}
diff --git a/src/AppInstallerCLICore/Workflows/FontFlow.h b/src/AppInstallerCLICore/Workflows/FontFlow.h
new file mode 100644
index 0000000000..c3346e1a60
--- /dev/null
+++ b/src/AppInstallerCLICore/Workflows/FontFlow.h
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include "ExecutionContext.h"
+
+namespace AppInstaller::CLI::Workflow
+{
+ // Reports the installed fonts as a table.
+ // Required Args: None
+ // Inputs: None
+ // Outputs: None
+ void ReportInstalledFonts(Execution::Context& context);
+}
diff --git a/src/AppInstallerCLIE2ETests/BaseCommand.cs b/src/AppInstallerCLIE2ETests/BaseCommand.cs
index 1b28227fd5..faa07c2dd0 100644
--- a/src/AppInstallerCLIE2ETests/BaseCommand.cs
+++ b/src/AppInstallerCLIE2ETests/BaseCommand.cs
@@ -1,4 +1,4 @@
-// -----------------------------------------------------------------------------
+// -----------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. Licensed under the MIT License.
//
@@ -6,10 +6,7 @@
namespace AppInstallerCLIE2ETests
{
- using System;
- using System.IO;
using AppInstallerCLIE2ETests.Helpers;
- using Newtonsoft.Json.Linq;
using NUnit.Framework;
///
diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
index 9f0f88b8df..46d093b80c 100644
--- a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
+++ b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw
@@ -3138,5 +3138,40 @@ Please specify one of them using the --source option to proceed.
Downloaded zero byte installer; ensure that your network connection is working properly.
+
+
+ Manage fonts
+
+
+ Manage fonts with sub-commands. Fonts can be installed, upgraded, or uninstalled similar to packages.
+
+
+ Family
+
+
+ Faces
+ "Faces" represents the typeface of the font family such as 'Bold' or 'Italic'
+
+
+ Filter results by family name
+
+
+ Face
+ "Face" represents the typeface of a font family such as 'Bold' or 'Italic'
+
+
+ Paths
+
+
+ No installed font found matching input criteria.
+
+
+ List installed fonts
+
+
+ List all installed fonts, or full details of a specific font.
+
+
+ Version
\ No newline at end of file
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
index 043eab3f5a..7500b2c1bb 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj
@@ -284,6 +284,7 @@
+
diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
index a24784a802..5d28140d17 100644
--- a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
+++ b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters
@@ -368,6 +368,9 @@
Source Files\Repository
+
+ Source Files\Common
+
diff --git a/src/AppInstallerCLITests/Fonts.cpp b/src/AppInstallerCLITests/Fonts.cpp
new file mode 100644
index 0000000000..81a50791e8
--- /dev/null
+++ b/src/AppInstallerCLITests/Fonts.cpp
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include "TestCommon.h"
+#include
+
+using namespace AppInstaller::Fonts;
+using namespace TestCommon;
+
+constexpr std::wstring_view s_testFontName = L"Times New Roman";
+
+TEST_CASE("GetInstalledFonts", "[fonts]")
+{
+ FontCatalog fontCatalog;
+ std::vector installedFontFamilies;
+ REQUIRE_NOTHROW(installedFontFamilies = fontCatalog.GetInstalledFontFamilies());
+ REQUIRE(installedFontFamilies.size() > 0);
+}
+
+TEST_CASE("GetSingleFontFamily", "[fonts]")
+{
+ FontCatalog fontCatalog;
+ std::vector fontFamily;
+ REQUIRE_NOTHROW(fontFamily = fontCatalog.GetInstalledFontFamilies(std::wstring(s_testFontName)));
+ REQUIRE_FALSE(fontFamily.empty());
+ FontFamily singleFontFamily = fontFamily[0];
+ REQUIRE(AppInstaller::Utility::CaseInsensitiveEquals(singleFontFamily.Name, s_testFontName));
+ REQUIRE(singleFontFamily.Faces.size() > 0);
+}
+
+TEST_CASE("GetInvalidFontFamily", "[fonts]")
+{
+ FontCatalog fontCatalog;
+ std::vector fontFamily;
+ REQUIRE_NOTHROW(fontFamily = fontCatalog.GetInstalledFontFamilies(L"Invalid Font"));
+ REQUIRE(fontFamily.empty());
+}
diff --git a/src/AppInstallerCLITests/Versions.cpp b/src/AppInstallerCLITests/Versions.cpp
index 0988d58074..54975eda49 100644
--- a/src/AppInstallerCLITests/Versions.cpp
+++ b/src/AppInstallerCLITests/Versions.cpp
@@ -421,3 +421,30 @@ TEST_CASE("SemanticVersion", "[versions]")
REQUIRE(version.BuildMetadata() == Version("4.5.6"));
REQUIRE(version.PartAt(2).Other == "-beta+4.5.6");
}
+
+TEST_CASE("OpenTypeFontVersion", "[versions]")
+{
+ // Valid font version.
+ OpenTypeFontVersion version = OpenTypeFontVersion("Version 1.234");
+ REQUIRE(version.ToString() == "1.234");
+ REQUIRE(version.GetParts().size() == 2);
+ REQUIRE(version.PartAt(0).Integer == 1);
+ REQUIRE(version.PartAt(1).Integer == 234);
+
+ // Font version with additional metadata.
+ version = OpenTypeFontVersion("Version 9.876.54 ;2024");
+ REQUIRE(version.ToString() == "9.876");
+ REQUIRE(version.GetParts().size() == 2);
+ REQUIRE(version.PartAt(0).Integer == 9);
+ REQUIRE(version.PartAt(1).Integer == 876);
+
+ // Invalid version. Font version must have at least 2 parts.
+ REQUIRE_NOTHROW(version = OpenTypeFontVersion("1234567"));
+ REQUIRE(version.IsUnknown());
+ REQUIRE(version.ToString() == "Unknown");
+
+ // Major and minor parts must have digits.
+ REQUIRE_NOTHROW(version = OpenTypeFontVersion(" abc.def "));
+ REQUIRE(version.IsUnknown());
+ REQUIRE(version.ToString() == "Unknown");
+}
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
index d57fcfc5e6..927547fc8f 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj
@@ -427,6 +427,7 @@
+
true
@@ -478,6 +479,7 @@
+
true
diff --git a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
index 3498d88dfb..1cb64a4d0e 100644
--- a/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
+++ b/src/AppInstallerCommonCore/AppInstallerCommonCore.vcxproj.filters
@@ -204,6 +204,9 @@
Public\winget
+
+ Public\winget
+
@@ -368,6 +371,9 @@
Source Files
+
+ Source Files
+
diff --git a/src/AppInstallerCommonCore/ExperimentalFeature.cpp b/src/AppInstallerCommonCore/ExperimentalFeature.cpp
index 733ff9ccd5..c3709c6802 100644
--- a/src/AppInstallerCommonCore/ExperimentalFeature.cpp
+++ b/src/AppInstallerCommonCore/ExperimentalFeature.cpp
@@ -48,6 +48,8 @@ namespace AppInstaller::Settings
return userSettings.Get();
case ExperimentalFeature::Feature::ConfigureExport:
return userSettings.Get();
+ case ExperimentalFeature::Feature::Font:
+ return userSettings.Get();
default:
THROW_HR(E_UNEXPECTED);
}
@@ -85,6 +87,9 @@ namespace AppInstaller::Settings
return ExperimentalFeature{ "Configure Self Elevation", "configureSelfElevate", "https://aka.ms/winget-settings", Feature::ConfigureSelfElevation };
case Feature::ConfigureExport:
return ExperimentalFeature{ "Configure Export", "configureExport", "https://aka.ms/winget-settings", Feature::ConfigureExport };
+ case Feature::Font:
+ return ExperimentalFeature{ "Font", "Font", "https://aka.ms/winget-settings", Feature::Font };
+
default:
THROW_HR(E_UNEXPECTED);
}
diff --git a/src/AppInstallerCommonCore/Fonts.cpp b/src/AppInstallerCommonCore/Fonts.cpp
new file mode 100644
index 0000000000..6f5f328f93
--- /dev/null
+++ b/src/AppInstallerCommonCore/Fonts.cpp
@@ -0,0 +1,175 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#include "pch.h"
+#include
+#include
+#include
+
+namespace AppInstaller::Fonts
+{
+ namespace
+ {
+ std::vector GetFontFilePaths(const wil::com_ptr& fontFace)
+ {
+ UINT32 fileCount = 0;
+ THROW_IF_FAILED(fontFace->GetFiles(&fileCount, nullptr));
+
+ static_assert(sizeof(wil::com_ptr) == sizeof(IDWriteFontFile*));
+ std::vector> fontFiles;
+ fontFiles.resize(fileCount);
+
+ THROW_IF_FAILED(fontFace->GetFiles(&fileCount, fontFiles[0].addressof()));
+
+ std::vector filePaths;
+ for (UINT32 i = 0; i < fileCount; ++i) {
+ wil::com_ptr loader;
+ THROW_IF_FAILED(fontFiles[i]->GetLoader(loader.addressof()));
+
+ const void* fontFileReferenceKey;
+ UINT32 fontFileReferenceKeySize;
+ THROW_IF_FAILED(fontFiles[i]->GetReferenceKey(&fontFileReferenceKey, &fontFileReferenceKeySize));
+
+ if (const auto localLoader = loader.try_query()) {
+ UINT32 pathLength;
+ THROW_IF_FAILED(localLoader->GetFilePathLengthFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &pathLength));
+ pathLength += 1; // Account for the trailing null terminator during allocation.
+
+ std::wstring path;
+ path.resize(pathLength);
+ THROW_IF_FAILED(localLoader->GetFilePathFromKey(fontFileReferenceKey, fontFileReferenceKeySize, &path[0], pathLength));
+ path.resize(pathLength - 1); // Remove the null char.
+ filePaths.emplace_back(std::move(path));
+ }
+ }
+
+ return filePaths;
+ }
+ }
+
+ FontCatalog::FontCatalog()
+ {
+ m_preferredLocales = AppInstaller::Locale::GetUserPreferredLanguagesUTF16();
+ }
+
+ std::vector FontCatalog::GetInstalledFontFamilies(std::optional familyName)
+ {
+ wil::com_ptr factory;
+ THROW_IF_FAILED(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(factory), factory.put_unknown()));
+
+ wil::com_ptr collection;
+ THROW_IF_FAILED(factory->GetSystemFontCollection(collection.addressof(), FALSE));
+
+ std::vector installedFontFamilies;
+
+ if (familyName.has_value())
+ {
+ UINT32 index;
+ BOOL exists;
+ THROW_IF_FAILED(collection->FindFamilyName(familyName.value().c_str(), &index, &exists));
+
+ if (exists)
+ {
+ installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index));
+ }
+ }
+ else
+ {
+ UINT32 familyCount = collection->GetFontFamilyCount();
+
+ for (UINT32 index = 0; index < familyCount; index++)
+ {
+ installedFontFamilies.emplace_back(GetFontFamilyByIndex(collection, index));
+ }
+ }
+
+ return installedFontFamilies;
+ }
+
+ std::wstring FontCatalog::GetLocalizedStringFromFont(const wil::com_ptr& localizedStringCollection)
+ {
+ UINT32 index = 0;
+ BOOL exists = false;
+
+ for (const auto& locale : m_preferredLocales)
+ {
+ if (SUCCEEDED_LOG(localizedStringCollection->FindLocaleName(locale.c_str(), &index, &exists)) && exists)
+ {
+ break;
+ }
+ }
+
+ // If the locale does not exist, resort to the default value at the 0 index.
+ if (!exists)
+ {
+ index = 0;
+ }
+
+ UINT32 length = 0;
+ THROW_IF_FAILED(localizedStringCollection->GetStringLength(index, &length));
+ length += 1; // Account for the trailing null terminator during allocation.
+
+ std::wstring localizedString;
+ localizedString.resize(length);
+ THROW_IF_FAILED(localizedStringCollection->GetString(index, &localizedString[0], length));
+ localizedString.resize(length - 1); // Remove the null char.
+ return localizedString;
+ }
+
+ std::wstring FontCatalog::GetFontFaceName(const wil::com_ptr& font)
+ {
+ wil::com_ptr faceNames;
+ THROW_IF_FAILED(font->GetFaceNames(faceNames.addressof()));
+ return GetLocalizedStringFromFont(faceNames);
+ }
+
+ std::wstring FontCatalog::GetFontFamilyName(const wil::com_ptr& fontFamily)
+ {
+ wil::com_ptr familyNames;
+ THROW_IF_FAILED(fontFamily->GetFamilyNames(familyNames.addressof()));
+ return GetLocalizedStringFromFont(familyNames);
+ }
+
+ Utility::OpenTypeFontVersion FontCatalog::GetFontFaceVersion(const wil::com_ptr& font)
+ {
+ wil::com_ptr fontVersion;
+ BOOL exists;
+ THROW_IF_FAILED(font->GetInformationalStrings(DWRITE_INFORMATIONAL_STRING_VERSION_STRINGS, fontVersion.addressof(), &exists));
+ if (!exists)
+ {
+ return {};
+ }
+
+ std::string value = AppInstaller::Utility::ConvertToUTF8(GetLocalizedStringFromFont(fontVersion));
+ Utility::OpenTypeFontVersion openTypeFontVersion{ value };
+ return openTypeFontVersion;
+ }
+
+ FontFamily FontCatalog::GetFontFamilyByIndex(const wil::com_ptr& collection, UINT32 index)
+ {
+ wil::com_ptr family;
+ THROW_IF_FAILED(collection->GetFontFamily(index, family.addressof()));
+ std::wstring familyName = GetFontFamilyName(family);
+
+ std::vector fontFaces;
+ UINT32 fontCount = family->GetFontCount();
+ for (UINT32 fontIndex = 0; fontIndex < fontCount; fontIndex++)
+ {
+ wil::com_ptr font;
+ THROW_IF_FAILED(family->GetFont(fontIndex, font.addressof()));
+
+ wil::com_ptr fontFace;
+ THROW_IF_FAILED(font->CreateFontFace(fontFace.addressof()));
+
+ FontFace fontFaceEntry;
+ fontFaceEntry.Name = GetFontFaceName(font);
+ fontFaceEntry.Version = GetFontFaceVersion(font);
+ fontFaceEntry.FilePaths = GetFontFilePaths(fontFace);
+ fontFaces.emplace_back(std::move(fontFaceEntry));
+ }
+
+ FontFamily fontFamily;
+ fontFamily.Name = std::move(familyName);
+ fontFamily.Faces = std::move(fontFaces);
+ return fontFamily;
+ }
+}
diff --git a/src/AppInstallerCommonCore/Locale.cpp b/src/AppInstallerCommonCore/Locale.cpp
index c308584226..ae8985910c 100644
--- a/src/AppInstallerCommonCore/Locale.cpp
+++ b/src/AppInstallerCommonCore/Locale.cpp
@@ -120,6 +120,18 @@ namespace AppInstaller::Locale
return result;
}
+ std::vector GetUserPreferredLanguagesUTF16()
+ {
+ std::vector result;
+
+ for (const auto& lang : winrt::Windows::System::UserProfile::GlobalizationPreferences::Languages())
+ {
+ result.emplace_back(std::wstring(lang));
+ }
+
+ return result;
+ }
+
std::string LocaleIdToBcp47Tag(LCID localeId)
{
WCHAR localeName[MAX_LOCALE_SNAME_LEN] = {0};
diff --git a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h
index 2b8a729a2d..cecadb24d8 100644
--- a/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h
+++ b/src/AppInstallerCommonCore/Public/AppInstallerRuntime.h
@@ -56,6 +56,10 @@ namespace AppInstaller::Runtime
CLIExecutable,
// The location of the image assets, if it exists.
ImageAssets,
+ // The location where fonts are installed with user scope.
+ FontsUserInstallLocation,
+ // The location where fonts are installed with machine scope.
+ FontsMachineInstallLocation,
// Always one more than the last path; for being able to iterate paths in tests.
Max
};
@@ -70,6 +74,9 @@ namespace AppInstaller::Runtime
return Filesystem::GetPathTo(path, forDisplay);
}
+ // Replaces the substring in the path with the user profile environment variable.
+ void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path);
+
// Gets a new temp file path.
std::filesystem::path GetNewTempFilePath();
diff --git a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h
index 733f1aa453..80469aa24c 100644
--- a/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h
+++ b/src/AppInstallerCommonCore/Public/winget/ExperimentalFeature.h
@@ -27,6 +27,7 @@ namespace AppInstaller::Settings
Configuration03 = 0x4,
ConfigureSelfElevation = 0x8,
ConfigureExport = 0x10,
+ Font = 0x20,
Max, // This MUST always be after all experimental features
// Features listed after Max will not be shown with the features command
diff --git a/src/AppInstallerCommonCore/Public/winget/Fonts.h b/src/AppInstallerCommonCore/Public/winget/Fonts.h
new file mode 100644
index 0000000000..443dea5f7a
--- /dev/null
+++ b/src/AppInstallerCommonCore/Public/winget/Fonts.h
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+#pragma once
+#include
+#include
+#include
+
+namespace AppInstaller::Fonts
+{
+ struct FontFace
+ {
+ std::wstring Name;
+ std::vector FilePaths;
+ Utility::OpenTypeFontVersion Version;
+ };
+
+ struct FontFamily
+ {
+ std::wstring Name;
+ std::vector Faces;
+ };
+
+ struct FontCatalog
+ {
+ FontCatalog();
+
+ // Gets all installed font families on the system. If an exact family name is provided and found, returns the installed font family.
+ std::vector GetInstalledFontFamilies(std::optional familyName = {});
+
+ private:
+ FontFamily GetFontFamilyByIndex(const wil::com_ptr& collection, UINT32 index);
+ std::wstring GetLocalizedStringFromFont(const wil::com_ptr& localizedStringCollection);
+ std::wstring GetFontFamilyName(const wil::com_ptr& fontFamily);
+ std::wstring GetFontFaceName(const wil::com_ptr& font);
+ Utility::OpenTypeFontVersion GetFontFaceVersion(const wil::com_ptr& font);
+
+ std::vector m_preferredLocales;
+ };
+}
diff --git a/src/AppInstallerCommonCore/Public/winget/Locale.h b/src/AppInstallerCommonCore/Public/winget/Locale.h
index f4cd7a2d54..5ae86a8de4 100644
--- a/src/AppInstallerCommonCore/Public/winget/Locale.h
+++ b/src/AppInstallerCommonCore/Public/winget/Locale.h
@@ -20,6 +20,9 @@ namespace AppInstaller::Locale
// Get the list of user Preferred Languages from settings. Returns an empty vector in rare cases of failure.
std::vector GetUserPreferredLanguages();
+ // Get the list of user Preferred Languages from settings. Returns an empty vector in rare cases of failure.
+ std::vector GetUserPreferredLanguagesUTF16();
+
// Get the bcp47 tag from a locale id. Returns empty string if conversion cannot be performed.
std::string LocaleIdToBcp47Tag(LCID localeId);
-}
\ No newline at end of file
+}
diff --git a/src/AppInstallerCommonCore/Public/winget/UserSettings.h b/src/AppInstallerCommonCore/Public/winget/UserSettings.h
index 5d8615485a..b580775c28 100644
--- a/src/AppInstallerCommonCore/Public/winget/UserSettings.h
+++ b/src/AppInstallerCommonCore/Public/winget/UserSettings.h
@@ -78,6 +78,7 @@ namespace AppInstaller::Settings
EFConfiguration03,
EFConfigureSelfElevation,
EFConfigureExport,
+ EFFonts,
// Telemetry
TelemetryDisable,
// Install behavior
@@ -161,6 +162,7 @@ namespace AppInstaller::Settings
SETTINGMAPPING_SPECIALIZATION(Setting::EFConfiguration03, bool, bool, false, ".experimentalFeatures.configuration03"sv);
SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureSelfElevation, bool, bool, false, ".experimentalFeatures.configureSelfElevate"sv);
SETTINGMAPPING_SPECIALIZATION(Setting::EFConfigureExport, bool, bool, false, ".experimentalFeatures.configureExport"sv);
+ SETTINGMAPPING_SPECIALIZATION(Setting::EFFonts, bool, bool, false, ".experimentalFeatures.fonts"sv);
// Telemetry
SETTINGMAPPING_SPECIALIZATION(Setting::TelemetryDisable, bool, bool, false, ".telemetry.disable"sv);
// Install behavior
diff --git a/src/AppInstallerCommonCore/Runtime.cpp b/src/AppInstallerCommonCore/Runtime.cpp
index 92bb493a83..3a8973d8e9 100644
--- a/src/AppInstallerCommonCore/Runtime.cpp
+++ b/src/AppInstallerCommonCore/Runtime.cpp
@@ -29,6 +29,7 @@ namespace AppInstaller::Runtime
constexpr std::string_view s_PortablePackageRoot = "WinGet"sv;
constexpr std::string_view s_PortablePackagesDirectory = "Packages"sv;
constexpr std::string_view s_LinksDirectory = "Links"sv;
+ constexpr std::string_view s_FontsInstallDirectory = "Microsoft\\Windows\\Fonts"sv;
// Use production CLSIDs as a surrogate for repository location.
#if USE_PROD_CLSIDS
constexpr std::string_view s_ImageAssetsDirectoryRelative = "Assets\\WinGet"sv;
@@ -162,15 +163,6 @@ namespace AppInstaller::Runtime
return result;
}
-
- // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE.
- void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path)
- {
- if (!ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_LocalAppData), s_LocalAppDataEnvironmentVariable))
- {
- ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_Profile), s_UserProfileEnvironmentVariable);
- }
- }
}
void SetRuntimePathStateName(std::string name)
@@ -240,6 +232,14 @@ namespace AppInstaller::Runtime
result.Path = GetKnownFolderPath(FOLDERID_Downloads);
mayBeInProfilePath = true;
break;
+ case PathName::FontsUserInstallLocation:
+ result.Path = GetKnownFolderPath(FOLDERID_LocalAppData);
+ result.Path /= s_FontsInstallDirectory;
+ mayBeInProfilePath = true;
+ break;
+ case PathName::FontsMachineInstallLocation:
+ result.Path = GetKnownFolderPath(FOLDERID_Fonts);
+ break;
default:
THROW_HR(E_UNEXPECTED);
}
@@ -314,6 +314,8 @@ namespace AppInstaller::Runtime
case PathName::PortableLinksUserLocation:
case PathName::PortablePackageUserRoot:
case PathName::UserProfileDownloads:
+ case PathName::FontsUserInstallLocation:
+ case PathName::FontsMachineInstallLocation:
result = GetPathDetailsCommon(path, forDisplay);
break;
case PathName::SelfPackageRoot:
@@ -418,6 +420,8 @@ namespace AppInstaller::Runtime
case PathName::PortableLinksUserLocation:
case PathName::PortablePackageUserRoot:
case PathName::UserProfileDownloads:
+ case PathName::FontsUserInstallLocation:
+ case PathName::FontsMachineInstallLocation:
result = GetPathDetailsCommon(path, forDisplay);
break;
case PathName::SelfPackageRoot:
@@ -476,6 +480,15 @@ namespace AppInstaller::Runtime
return result;
}
+ // Try to replace LOCALAPPDATA first as it is the likely location, fall back to trying USERPROFILE.
+ void ReplaceProfilePathsWithEnvironmentVariable(std::filesystem::path& path)
+ {
+ if (!ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_LocalAppData), s_LocalAppDataEnvironmentVariable))
+ {
+ ReplaceCommonPathPrefix(path, GetKnownFolderPath(FOLDERID_Profile), s_UserProfileEnvironmentVariable);
+ }
+ }
+
std::filesystem::path GetNewTempFilePath()
{
GUID guid;
diff --git a/src/AppInstallerCommonCore/UserSettings.cpp b/src/AppInstallerCommonCore/UserSettings.cpp
index 476971d2f0..9e13610537 100644
--- a/src/AppInstallerCommonCore/UserSettings.cpp
+++ b/src/AppInstallerCommonCore/UserSettings.cpp
@@ -269,6 +269,7 @@ namespace AppInstaller::Settings
WINGET_VALIDATE_PASS_THROUGH(EFConfiguration03)
WINGET_VALIDATE_PASS_THROUGH(EFConfigureSelfElevation)
WINGET_VALIDATE_PASS_THROUGH(EFConfigureExport)
+ WINGET_VALIDATE_PASS_THROUGH(EFFonts)
WINGET_VALIDATE_PASS_THROUGH(AnonymizePathForDisplay)
WINGET_VALIDATE_PASS_THROUGH(TelemetryDisable)
WINGET_VALIDATE_PASS_THROUGH(InteractivityDisable)
diff --git a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h
index 569a33aa32..9df7cab413 100644
--- a/src/AppInstallerSharedLib/Public/AppInstallerVersions.h
+++ b/src/AppInstallerSharedLib/Public/AppInstallerVersions.h
@@ -281,4 +281,20 @@ namespace AppInstaller::Utility
// Checks if there are overlaps within the list of version ranges
bool HasOverlapInVersionRanges(const std::vector& ranges);
+
+ // The OpenType font version.
+ // The format of this version type is 'Version 1.234 ;567'
+ // The only part that is of importance is the 'Major.Minor' parts.
+ // The 'Version' string is typically found at the beginning of the version string.
+ // Any value after a digit that is not a '.' represents some other meaning.
+ struct OpenTypeFontVersion : Version
+ {
+ OpenTypeFontVersion() = default;
+
+ OpenTypeFontVersion(std::string&& version);
+ OpenTypeFontVersion(const std::string& version) :
+ OpenTypeFontVersion(std::string(version)) {}
+
+ void Assign(std::string version, std::string_view splitChars = DefaultSplitChars) override;
+ };
}
diff --git a/src/AppInstallerSharedLib/Versions.cpp b/src/AppInstallerSharedLib/Versions.cpp
index 6b02675d1d..2c9972c561 100644
--- a/src/AppInstallerSharedLib/Versions.cpp
+++ b/src/AppInstallerSharedLib/Versions.cpp
@@ -661,4 +661,64 @@ namespace AppInstaller::Utility
return false;
}
+
+ OpenTypeFontVersion::OpenTypeFontVersion(std::string&& version)
+ {
+ Assign(std::move(version), DefaultSplitChars);
+ }
+
+ void OpenTypeFontVersion::Assign(std::string version, std::string_view splitChars)
+ {
+ // Open type version requires using the default split character
+ THROW_HR_IF(E_INVALIDARG, splitChars != DefaultSplitChars);
+
+ // Split on default split character.
+ std::vector parts = Split(version, '.', true);
+
+ std::string majorString;
+ std::string minorString;
+
+ // Font version must have a "major.minor" part.
+ if (parts.size() >= 2)
+ {
+ // Find first digit and trim all preceding characters.
+ std::string firstPart = parts[0];
+ size_t majorStartIndex = firstPart.find_first_of(s_Digit_Characters);
+
+ if (majorStartIndex != std::string::npos)
+ {
+ firstPart.erase(0, majorStartIndex);
+ }
+
+ size_t majorEndIndex = firstPart.find_last_of(s_Digit_Characters);
+ majorString = firstPart.substr(0, majorEndIndex + 1);
+
+ // Parse and verify minor part.
+ std::string secondPart = parts[1];
+ size_t endPos = secondPart.find_first_not_of(s_Digit_Characters);
+
+ // If a non-digit character exists, trim off the remainder.
+ if (endPos != std::string::npos)
+ {
+ secondPart.erase(endPos, secondPart.length());
+ }
+
+ minorString = secondPart;
+ }
+
+ // Verify results.
+ if (!majorString.empty() && !minorString.empty())
+ {
+ m_parts.emplace_back(majorString);
+ m_parts.emplace_back(minorString);
+ m_version = Utility::Join(DefaultSplitChars, { majorString, minorString });
+
+ Trim();
+ }
+ else
+ {
+ m_version = s_Version_Part_Unknown;
+ m_parts.emplace_back(0, std::string{ s_Version_Part_Unknown });
+ }
+ }
}