Skip to content

Commit

Permalink
Introduce a basic ApplicationState class
Browse files Browse the repository at this point in the history
  • Loading branch information
lhecker committed Jun 25, 2021
1 parent 8c00dd7 commit 7d45da3
Show file tree
Hide file tree
Showing 11 changed files with 476 additions and 219 deletions.
14 changes: 7 additions & 7 deletions doc/cascadia/AddASetting.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Adding a setting to Windows Terminal is fairly straightforward. This guide serve

The Terminal Settings Model (`Microsoft.Terminal.Settings.Model`) is responsible for (de)serializing and exposing settings.

### `GETSET_SETTING` macro
### `INHERITABLE_SETTING` macro

The `GETSET_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
The `INHERITABLE_SETTING` macro can be used to implement inheritance for your new setting and store the setting in the settings model. It takes three parameters:
- `type`: the type that the setting will be stored as
- `name`: the name of the variable for storage
- `defaultValue`: the value to use if the user does not define the setting anywhere
Expand All @@ -20,7 +20,7 @@ This tutorial will add `CloseOnExitMode CloseOnExit` as a profile setting.
1. In `Profile.h`, declare/define the setting:

```c++
GETSET_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
INHERITABLE_SETTING(CloseOnExitMode, CloseOnExit, CloseOnExitMode::Graceful)
```
2. In `Profile.idl`, expose the setting via WinRT:
Expand Down Expand Up @@ -141,7 +141,7 @@ struct OpenSettingsArgs : public OpenSettingsArgsT<OpenSettingsArgs>
OpenSettingsArgs() = default;

// adds a getter/setter for your argument, and defines the json key
GETSET_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
WINRT_PROPERTY(SettingsTarget, Target, SettingsTarget::SettingsFile);
static constexpr std::string_view TargetKey{ "target" };

public:
Expand Down Expand Up @@ -213,9 +213,9 @@ Terminal-level settings are settings that affect a shell session. Generally, the
- Declare the setting in `IControlSettings.idl` or `ICoreSettings.idl` (whichever is relevant to your setting). If your setting is an enum setting, declare the enum here instead of in the `TerminalSettingsModel` project.
- In `TerminalSettings.h`, declare/define the setting...
```c++
// The GETSET_PROPERTY macro declares/defines a getter setter for the setting.
// Like GETSET_SETTING, it takes in a type, name, and defaultValue.
GETSET_PROPERTY(bool, UseAcrylic, false);
// The WINRT_PROPERTY macro declares/defines a getter setter for the setting.
// Like INHERITABLE_SETTING, it takes in a type, name, and defaultValue.
WINRT_PROPERTY(bool, UseAcrylic, false);
```
- In `TerminalSettings.cpp`...
- update `_ApplyProfileSettings` for profile settings
Expand Down
85 changes: 30 additions & 55 deletions src/cascadia/TerminalApp/AppLogic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
}
});
}

// Method Description:
Expand Down Expand Up @@ -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,
[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
Expand Down
16 changes: 8 additions & 8 deletions src/cascadia/TerminalApp/AppLogic.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
#include "FindTargetWindowResult.g.h"
#include "TerminalPage.h"
#include "Jumplist.h"
#include "../../cascadia/inc/cppwinrt_utils.h"

#include <inc/cppwinrt_utils.h>
#include <ThrottledFunc.h>

#ifdef UNIT_TESTING
// fwdecl unittest classes
Expand Down Expand Up @@ -111,17 +113,15 @@ namespace winrt::TerminalApp::implementation

Microsoft::Terminal::Settings::Model::CascadiaSettings _settings{ nullptr };

HRESULT _settingsLoadedResult;
winrt::hstring _settingsLoadExceptionText{};

bool _loadedInitialSettings;

wil::unique_folder_change_reader_nothrow _reader;
std::shared_ptr<ThrottledFuncTrailing<>> _reloadSettings;
til::throttled_func_trailing<> _reloadState;
winrt::hstring _settingsLoadExceptionText;
HRESULT _settingsLoadedResult = S_OK;
bool _loadedInitialSettings = false;

std::shared_mutex _dialogLock;

std::atomic<bool> _settingsReloadQueued{ false };

::TerminalApp::AppCommandlineArgs _appArgs;
::TerminalApp::AppCommandlineArgs _settingsAppArgs;
static TerminalApp::FindTargetWindowResult _doFindTargetWindow(winrt::array_view<const hstring> args,
Expand Down
172 changes: 172 additions & 0 deletions src/cascadia/TerminalSettingsModel/ApplicationState.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// 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"

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("std::unordered_set<{}>", 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() / L"state.json");
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 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.
// * Doesn't acquire any locks - may only be called by ApplicationState's constructor.
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()
}
Loading

1 comment on commit 7d45da3

@github-actions

This comment was marked as outdated.

Please sign in to comment.