-
Notifications
You must be signed in to change notification settings - Fork 8.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introduce a basic ApplicationState class #10513
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -189,9 +189,7 @@ namespace winrt::TerminalApp::implementation | |
} | ||
|
||
AppLogic::AppLogic() : | ||
_dialogLock{}, | ||
_loadedInitialSettings{ false }, | ||
_settingsLoadedResult{ S_OK } | ||
_reloadState{ std::chrono::milliseconds(100), []() { ApplicationState::SharedInstance().Reload(); } } | ||
{ | ||
// For your own sanity, it's better to do setup outside the ctor. | ||
// If you do any setup in the ctor that ends up throwing an exception, | ||
|
@@ -204,6 +202,13 @@ namespace winrt::TerminalApp::implementation | |
// SetTitleBarContent | ||
_isElevated = _isUserAdmin(); | ||
_root = winrt::make_self<TerminalPage>(); | ||
|
||
_reloadSettings = std::make_shared<ThrottledFuncTrailing<>>(_root->Dispatcher(), std::chrono::milliseconds(100), [weakSelf = get_weak()]() { | ||
if (auto self{ weakSelf.get() }) | ||
{ | ||
self->_ReloadSettings(); | ||
} | ||
}); | ||
Comment on lines
+206
to
+211
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This replaces the previous, manual settings.json load delay & throttling we had before. |
||
} | ||
|
||
// Method Description: | ||
|
@@ -859,59 +864,29 @@ namespace winrt::TerminalApp::implementation | |
// - <none> | ||
void AppLogic::_RegisterSettingsChange() | ||
{ | ||
// Get the containing folder. | ||
const std::filesystem::path settingsPath{ std::wstring_view{ CascadiaSettings::SettingsPath() } }; | ||
const auto folder = settingsPath.parent_path(); | ||
|
||
_reader.create(folder.c_str(), | ||
false, | ||
wil::FolderChangeEvents::All, | ||
[this, settingsPath](wil::FolderChangeEvent event, PCWSTR fileModified) { | ||
// We want file modifications, AND when files are renamed to be | ||
// settings.json. This second case will oftentimes happen with text | ||
// editors, who will write a temp file, then rename it to be the | ||
// actual file you wrote. So listen for that too. | ||
if (!(event == wil::FolderChangeEvent::Modified || | ||
event == wil::FolderChangeEvent::RenameNewName || | ||
event == wil::FolderChangeEvent::Removed)) | ||
{ | ||
return; | ||
} | ||
|
||
std::filesystem::path modifiedFilePath = fileModified; | ||
|
||
// Getting basename (filename.ext) | ||
const auto settingsBasename = settingsPath.filename(); | ||
const auto modifiedBasename = modifiedFilePath.filename(); | ||
|
||
if (settingsBasename == modifiedBasename) | ||
{ | ||
this->_DispatchReloadSettings(); | ||
} | ||
}); | ||
} | ||
|
||
// Method Description: | ||
// - Dispatches a settings reload with debounce. | ||
// Text editors implement Save in a bunch of different ways, so | ||
// this stops us from reloading too many times or too quickly. | ||
fire_and_forget AppLogic::_DispatchReloadSettings() | ||
{ | ||
if (_settingsReloadQueued.exchange(true)) | ||
{ | ||
co_return; | ||
} | ||
|
||
auto weakSelf = get_weak(); | ||
|
||
co_await winrt::resume_after(std::chrono::milliseconds(100)); | ||
co_await winrt::resume_foreground(_root->Dispatcher()); | ||
|
||
if (auto self{ weakSelf.get() }) | ||
{ | ||
_ReloadSettings(); | ||
_settingsReloadQueued.store(false); | ||
} | ||
const std::filesystem::path statePath{ std::wstring_view{ ApplicationState::SharedInstance().FilePath() } }; | ||
|
||
_reader.create( | ||
settingsPath.parent_path().c_str(), | ||
false, | ||
// We want file modifications, AND when files are renamed to be | ||
// settings.json. This second case will oftentimes happen with text | ||
// editors, who will write a temp file, then rename it to be the | ||
// actual file you wrote. So listen for that too. | ||
wil::FolderChangeEvents::FileName | wil::FolderChangeEvents::LastWriteTime, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We previously listened for
|
||
[this, settingsBasename = settingsPath.filename(), stateBasename = statePath.filename()](wil::FolderChangeEvent, PCWSTR fileModified) { | ||
const auto modifiedBasename = std::filesystem::path{ fileModified }.filename(); | ||
|
||
if (modifiedBasename == settingsBasename) | ||
{ | ||
_reloadSettings->Run(); | ||
} | ||
else if (modifiedBasename == stateBasename) | ||
{ | ||
_reloadState(); | ||
} | ||
}); | ||
} | ||
|
||
void AppLogic::_ApplyLanguageSettingChange() noexcept | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT license. | ||
|
||
#include "pch.h" | ||
#include "ApplicationState.h" | ||
#include "CascadiaSettings.h" | ||
#include "ApplicationState.g.cpp" | ||
|
||
#include "JsonUtils.h" | ||
#include "FileUtils.h" | ||
|
||
constexpr std::wstring_view stateFileName{ L"state.json" }; | ||
|
||
namespace Microsoft::Terminal::Settings::Model::JsonUtils | ||
{ | ||
// This trait exists in order to serialize the std::unordered_set for GeneratedProfiles. | ||
template<typename T> | ||
struct ConversionTrait<std::unordered_set<T>> | ||
{ | ||
std::unordered_set<T> FromJson(const Json::Value& json) const | ||
{ | ||
ConversionTrait<T> trait; | ||
std::unordered_set<T> val; | ||
val.reserve(json.size()); | ||
|
||
for (const auto& element : json) | ||
{ | ||
val.emplace(trait.FromJson(element)); | ||
} | ||
|
||
return val; | ||
} | ||
|
||
bool CanConvert(const Json::Value& json) const | ||
{ | ||
ConversionTrait<T> trait; | ||
return json.isArray() && std::all_of(json.begin(), json.end(), [trait](const auto& json) -> bool { return trait.CanConvert(json); }); | ||
} | ||
|
||
Json::Value ToJson(const std::unordered_set<T>& val) | ||
{ | ||
ConversionTrait<T> trait; | ||
Json::Value json{ Json::arrayValue }; | ||
|
||
for (const auto& key : val) | ||
{ | ||
json.append(trait.ToJson(key)); | ||
} | ||
|
||
return json; | ||
} | ||
|
||
std::string TypeDescription() const | ||
{ | ||
return fmt::format("{}[]", ConversionTrait<GUID>{}.TypeDescription()); | ||
} | ||
}; | ||
} | ||
|
||
using namespace ::Microsoft::Terminal::Settings::Model; | ||
|
||
namespace winrt::Microsoft::Terminal::Settings::Model::implementation | ||
{ | ||
// Returns the application-global ApplicationState object. | ||
Microsoft::Terminal::Settings::Model::ApplicationState ApplicationState::SharedInstance() | ||
{ | ||
static auto state = winrt::make_self<ApplicationState>(GetBaseSettingsPath() / stateFileName); | ||
return *state; | ||
} | ||
|
||
ApplicationState::ApplicationState(std::filesystem::path path) noexcept : | ||
_path{ std::move(path) }, | ||
_throttler{ std::chrono::seconds(1), [this]() { _write(); } } | ||
{ | ||
_read(); | ||
} | ||
|
||
// The destructor ensures that the last write is flushed to disk before returning. | ||
ApplicationState::~ApplicationState() | ||
{ | ||
// This will ensure that we not just cancel the last outstanding timer, | ||
// but instead force it to run as soon as possible and wait for it to complete. | ||
_throttler.flush(); | ||
} | ||
|
||
// Re-read the state.json from disk. | ||
void ApplicationState::Reload() const noexcept | ||
{ | ||
_read(); | ||
} | ||
|
||
// Returns the state.json path on the disk. | ||
winrt::hstring ApplicationState::FilePath() const noexcept | ||
{ | ||
return winrt::hstring{ _path.wstring() }; | ||
} | ||
|
||
// Generate all getter/setters | ||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) \ | ||
type ApplicationState::name() const noexcept \ | ||
{ \ | ||
const auto state = _state.lock_shared(); \ | ||
const auto& value = state->name; \ | ||
return value ? *value : type{ __VA_ARGS__ }; \ | ||
} \ | ||
\ | ||
void ApplicationState::name(const type& value) noexcept \ | ||
{ \ | ||
{ \ | ||
auto state = _state.lock(); \ | ||
state->name.emplace(value); \ | ||
} \ | ||
\ | ||
_throttler(); \ | ||
} | ||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) | ||
#undef MTSM_APPLICATION_STATE_GEN | ||
|
||
// Deserializes the state.json at _path into this ApplicationState. | ||
// * ANY errors during app state will result in the creation of a new empty state. | ||
// * ANY errors during runtime will result in changes being partially ignored. | ||
void ApplicationState::_read() const noexcept | ||
try | ||
{ | ||
const auto data = ReadUTF8FileIfExists(_path).value_or(std::string{}); | ||
if (data.empty()) | ||
{ | ||
return; | ||
} | ||
|
||
std::string errs; | ||
std::unique_ptr<Json::CharReader> reader{ Json::CharReaderBuilder::CharReaderBuilder().newCharReader() }; | ||
|
||
Json::Value root; | ||
if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) | ||
{ | ||
throw winrt::hresult_error(WEB_E_INVALID_JSON_STRING, winrt::to_hstring(errs)); | ||
} | ||
|
||
auto state = _state.lock(); | ||
// GetValueForKey() comes in two variants: | ||
// * take a std::optional<T> reference | ||
// * return std::optional<T> by value | ||
// At the time of writing the former version skips missing fields in the json, | ||
// but we want to explicitly clear state fields that were removed from state.json. | ||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) state->name = JsonUtils::GetValueForKey<std::optional<type>>(root, key); | ||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) | ||
#undef MTSM_APPLICATION_STATE_GEN | ||
} | ||
CATCH_LOG() | ||
|
||
// Serialized this ApplicationState (in `context`) into the state.json at _path. | ||
// * Errors are only logged. | ||
// * _state->_writeScheduled is set to false, signaling our | ||
// setters that _synchronize() needs to be called again. | ||
void ApplicationState::_write() const noexcept | ||
try | ||
{ | ||
Json::Value root{ Json::objectValue }; | ||
|
||
{ | ||
auto state = _state.lock_shared(); | ||
#define MTSM_APPLICATION_STATE_GEN(type, name, key, ...) JsonUtils::SetValueForKey(root, key, state->name); | ||
MTSM_APPLICATION_STATE_FIELDS(MTSM_APPLICATION_STATE_GEN) | ||
#undef MTSM_APPLICATION_STATE_GEN | ||
} | ||
|
||
Json::StreamWriterBuilder wbuilder; | ||
const auto content = Json::writeString(wbuilder, root); | ||
WriteUTF8FileAtomic(_path, content); | ||
} | ||
CATCH_LOG() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the existing settings-reload throttler, this does the same but for state.json.
Since
til::throttled_func
cannot be copied or moved, this type must be initialized in the member list initializer (instead of more ergonomically in the constructor function body).