// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include #include "CascadiaSettings.h" #include "AppKeyBindingsSerialization.h" #include "../../types/inc/utils.hpp" #include #include using namespace ::TerminalApp; using namespace winrt::Microsoft::Terminal::TerminalControl; using namespace winrt::TerminalApp; using namespace winrt::Windows::Data::Json; using namespace winrt::Windows::Storage; using namespace winrt::Windows::Storage::Streams; using namespace ::Microsoft::Console; static constexpr std::wstring_view FILENAME{ L"profiles.json" }; static constexpr std::wstring_view SETTINGS_FOLDER_NAME{ L"\\Microsoft\\Windows Terminal\\" }; static constexpr std::string_view ProfilesKey{ "profiles" }; static constexpr std::string_view KeybindingsKey{ "keybindings" }; static constexpr std::string_view GlobalsKey{ "globals" }; static constexpr std::string_view SchemesKey{ "schemes" }; // Method Description: // - Creates a CascadiaSettings from whatever's saved on disk, or instantiates // a new one with the default values. If we're running as a packaged app, // it will load the settings from our packaged localappdata. If we're // running as an unpackaged application, it will read it from the path // we've set under localappdata. // Arguments: // - saveOnLoad: If true, we'll write the settings back out after we load them, // to make sure the schema is updated. // Return Value: // - a unique_ptr containing a new CascadiaSettings object. std::unique_ptr CascadiaSettings::LoadAll(const bool saveOnLoad) { std::unique_ptr resultPtr; std::optional fileData = _IsPackaged() ? _LoadAsPackagedApp() : _LoadAsUnpackagedApp(); const bool foundFile = fileData.has_value(); if (foundFile) { const auto actualData = fileData.value(); // Parse the json data. Json::Value root; std::unique_ptr reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; std::string errs; // This string will recieve any error text from failing to parse. // `parse` will return false if it fails. if (!reader->parse(actualData.c_str(), actualData.c_str() + actualData.size(), &root, &errs)) { // TODO:GH#990 display this exception text to the user, in a // copy-pasteable way. throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); } resultPtr = FromJson(root); if (saveOnLoad) { // Logically compare the json we've parsed from the file to what // we'd serialize at runtime. If the values are different, then // write the updated schema back out. const Json::Value reserialized = resultPtr->ToJson(); if (reserialized != root) { resultPtr->SaveAll(); } } } else { resultPtr = std::make_unique(); resultPtr->CreateDefaults(); // The settings file does not exist. Let's commit one. resultPtr->SaveAll(); } return resultPtr; } // Method Description: // - Serialize this settings structure, and save it to a file. The location of // the file changes depending whether we're running as a packaged // application or not. // Arguments: // - // Return Value: // - void CascadiaSettings::SaveAll() const { const auto json = ToJson(); Json::StreamWriterBuilder wbuilder; // Use 4 spaces to indent instead of \t wbuilder.settings_["indentation"] = " "; const auto serializedString = Json::writeString(wbuilder, json); if (_IsPackaged()) { _SaveAsPackagedApp(serializedString); } else { _SaveAsUnpackagedApp(serializedString); } } // Method Description: // - Serialize this object to a JsonObject. // Arguments: // - // Return Value: // - a JsonObject which is an equivalent serialization of this object. Json::Value CascadiaSettings::ToJson() const { Json::Value root; Json::Value profilesArray; for (const auto& profile : _profiles) { profilesArray.append(profile.ToJson()); } Json::Value schemesArray; const auto& colorSchemes = _globals.GetColorSchemes(); for (auto& scheme : colorSchemes) { schemesArray.append(scheme.ToJson()); } root[GlobalsKey.data()] = _globals.ToJson(); root[ProfilesKey.data()] = profilesArray; root[SchemesKey.data()] = schemesArray; return root; } // Method Description: // - Create a new instance of this class from a serialized JsonObject. // Arguments: // - json: an object which should be a serialization of a CascadiaSettings object. // Return Value: // - a new CascadiaSettings instance created from the values in `json` std::unique_ptr CascadiaSettings::FromJson(const Json::Value& json) { std::unique_ptr resultPtr = std::make_unique(); if (auto globals{ json[GlobalsKey.data()] }) { if (globals.isObject()) { resultPtr->_globals = GlobalAppSettings::FromJson(globals); } } else { // If there's no globals key in the root object, then try looking at the // root object for those properties instead, to gracefully upgrade. // This will attempt to do the legacy keybindings loading too resultPtr->_globals = GlobalAppSettings::FromJson(json); // If we didn't find keybindings in the legacy path, then they probably // don't exist in the file. Create the default keybindings if we // couldn't find any keybindings. auto keybindings{ json[KeybindingsKey.data()] }; if (!keybindings) { resultPtr->_CreateDefaultKeybindings(); } } // TODO:MSFT:20737698 - Display an error if we failed to parse settings // What should we do here if these keys aren't found?For default profile, // we could always pick the first profile and just set that as the default. // Finding no schemes is probably fine, unless of course one profile // references a scheme. We could fail with come error saying the // profiles file is corrupted. // Not having any profiles is also bad - should we say the file is corrupted? // Or should we just recreate the default profiles? auto& resultSchemes = resultPtr->_globals.GetColorSchemes(); if (auto schemes{ json[SchemesKey.data()] }) { for (auto schemeJson : schemes) { if (schemeJson.isObject()) { auto scheme = ColorScheme::FromJson(schemeJson); resultSchemes.emplace_back(std::move(scheme)); } } } if (auto profiles{ json[ProfilesKey.data()] }) { for (auto profileJson : profiles) { if (profileJson.isObject()) { auto profile = Profile::FromJson(profileJson); resultPtr->_profiles.emplace_back(profile); } } } return resultPtr; } // Function Description: // - Returns true if we're running in a packaged context. If we are, then we // have to use the Windows.Storage API's to save/load our files. If we're // not, then we won't be able to use those API's. // Arguments: // - // Return Value: // - true iff we're running in a packaged context. bool CascadiaSettings::_IsPackaged() { UINT32 length = 0; LONG rc = GetCurrentPackageFullName(&length, NULL); return rc != APPMODEL_ERROR_NO_PACKAGE; } // Method Description: // - Writes the given content to our settings file as UTF-8 encoded using the Windows.Storage // APIS's. This will only work within the context of an application with // package identity, so make sure to call _IsPackaged before calling this method. // Will overwrite any existing content in the file. // Arguments: // - content: the given string of content to write to the file. // Return Value: // - void CascadiaSettings::_SaveAsPackagedApp(const std::string& content) { auto curr = ApplicationData::Current(); auto folder = curr.RoamingFolder(); auto file_async = folder.CreateFileAsync(FILENAME, CreationCollisionOption::ReplaceExisting); auto file = file_async.get(); DataWriter dw = DataWriter(); const char* firstChar = content.c_str(); const char* lastChar = firstChar + content.size(); const uint8_t* firstByte = reinterpret_cast(firstChar); const uint8_t* lastByte = reinterpret_cast(lastChar); winrt::array_view bytes{ firstByte, lastByte }; dw.WriteBytes(bytes); FileIO::WriteBufferAsync(file, dw.DetachBuffer()).get(); } // Method Description: // - Writes the given content in UTF-8 to our settings file using the Win32 APIS's. // Will overwrite any existing content in the file. // Arguments: // - content: the given string of content to write to the file. // Return Value: // - // This can throw an exception if we fail to open the file for writing, or we // fail to write the file void CascadiaSettings::_SaveAsUnpackagedApp(const std::string& content) { // Get path to output file // In this scenario, the settings file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile(); auto hOut = CreateFileW(pathToSettingsFile.c_str(), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hOut == INVALID_HANDLE_VALUE) { THROW_LAST_ERROR(); } THROW_LAST_ERROR_IF(!WriteFile(hOut, content.c_str(), gsl::narrow(content.length()), 0, 0)); CloseHandle(hOut); } // Method Description: // - Computes the path to the settings file if the app is run unpackaged. // Will create any intermediate directories if they don't exist. // The file will end up under e.g. C:\Users\admin\AppData\Roaming\Microsoft\Windows Terminal\profiles.json // Arguments: // - // Return Value: // - A string containing the path to the unpackaged settings file // This can throw an exception if it fails to get the roaming app data folder. std::wstring CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() { wil::unique_cotaskmem_string roamingAppDataFolder; if (FAILED(SHGetKnownFolderPath(FOLDERID_RoamingAppData, 0, 0, &roamingAppDataFolder))) { THROW_LAST_ERROR(); } std::wstring parentDirectoryForSettingsFile(roamingAppDataFolder.get()); parentDirectoryForSettingsFile.append(SETTINGS_FOLDER_NAME); // Create the directory if it doesn't exist wil::CreateDirectoryDeep(parentDirectoryForSettingsFile.c_str()); std::wstring pathToSettingsFile(parentDirectoryForSettingsFile); pathToSettingsFile.append(FILENAME); return pathToSettingsFile; } // Method Description: // - Reads the content of our settings file using the Windows.Storage // APIS's. This will only work within the context of an application with // package identity, so make sure to call _IsPackaged before calling this method. // Arguments: // - // Return Value: // - an optional with the content of the file if we were able to open it, // otherwise the optional will be empty std::optional CascadiaSettings::_LoadAsPackagedApp() { auto curr = ApplicationData::Current(); auto folder = curr.RoamingFolder(); auto file_async = folder.TryGetItemAsync(FILENAME); auto file = file_async.get(); if (file == nullptr) { return std::nullopt; } const auto storageFile = file.as(); // settings file is UTF-8 without BOM auto buffer = FileIO::ReadBufferAsync(storageFile).get(); auto bufferData = buffer.data(); std::vector bytes{ bufferData, bufferData + buffer.Length() }; std::string resultString{ bytes.begin(), bytes.end() }; return { resultString }; } // Method Description: // - Reads the content in UTF-8 enconding of our settings file using the Win32 APIs // Arguments: // - // Return Value: // - an optional with the content of the file if we were able to open it, // otherwise the optional will be empty. // If the file exists, but we fail to read it, this can throw an exception // from reading the file std::optional CascadiaSettings::_LoadAsUnpackagedApp() { std::wstring pathToSettingsFile = CascadiaSettings::_GetFullPathToUnpackagedSettingsFile(); const auto hFile = CreateFileW(pathToSettingsFile.c_str(), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { // If the file doesn't exist, that's fine. Just log the error and return // nullopt - we'll create the defaults. LOG_LAST_ERROR(); return std::nullopt; } // fileSize is in bytes const auto fileSize = GetFileSize(hFile, nullptr); THROW_LAST_ERROR_IF(fileSize == INVALID_FILE_SIZE); auto utf8buffer = std::make_unique(fileSize); DWORD bytesRead = 0; THROW_LAST_ERROR_IF(!ReadFile(hFile, utf8buffer.get(), fileSize, &bytesRead, nullptr)); CloseHandle(hFile); // convert buffer to UTF-8 string std::string utf8string(utf8buffer.get(), fileSize); return { utf8string }; } // function Description: // - Returns the full path to the settings file, either within the application // package, or in its unpackaged location. // Arguments: // - // Return Value: // - the full path to the settings file winrt::hstring CascadiaSettings::GetSettingsPath() { return _IsPackaged() ? CascadiaSettings::_GetPackagedSettingsPath() : winrt::hstring{ CascadiaSettings::_GetFullPathToUnpackagedSettingsFile() }; } // Function Description: // - Get the full path to settings file in its packaged location. // Arguments: // - // Return Value: // - the full path to the packaged settings file. winrt::hstring CascadiaSettings::_GetPackagedSettingsPath() { const auto curr = ApplicationData::Current(); const auto folder = curr.RoamingFolder(); const auto file_async = folder.TryGetItemAsync(FILENAME); const auto file = file_async.get(); return file.Path(); }