554 lines
23 KiB
C++
554 lines
23 KiB
C++
#include "user_data.h"
|
|
|
|
#include <izo/Paths.hpp>
|
|
|
|
extern "C"
|
|
{
|
|
#include <ikv.h>
|
|
}
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <iomanip>
|
|
#include <optional>
|
|
#include <sstream>
|
|
#include <utility>
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <wincrypt.h>
|
|
#endif
|
|
|
|
namespace
|
|
{
|
|
std::filesystem::path roaming_directory()
|
|
{
|
|
if (const auto config = izo::GetKnownPath(izo::KnownPath::Config); !config.empty())
|
|
return config;
|
|
if (const auto temporary = izo::GetKnownPath(izo::KnownPath::Temporary); !temporary.empty())
|
|
return temporary;
|
|
return std::filesystem::temp_directory_path();
|
|
}
|
|
|
|
const ikv_node_t *object_value(const ikv_node_t *object, const char *key, ikv_type_t type)
|
|
{
|
|
const ikv_node_t *value = object ? ikv_object_get(object, key) : nullptr;
|
|
return value && ikv_node_type(value) == type ? value : nullptr;
|
|
}
|
|
|
|
std::optional<std::pair<std::string, std::string>> splitCredentialPayload(const std::string &payload)
|
|
{
|
|
const size_t separator = payload.find('\n');
|
|
if (separator == std::string::npos)
|
|
return std::nullopt;
|
|
return std::pair<std::string, std::string>{
|
|
payload.substr(0, separator),
|
|
payload.substr(separator + 1)};
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
std::string hexEncode(const std::string &value)
|
|
{
|
|
static constexpr char digits[] = "0123456789abcdef";
|
|
std::string encoded;
|
|
encoded.reserve(value.size() * 2);
|
|
for (const unsigned char byte : value)
|
|
{
|
|
encoded.push_back(digits[(byte >> 4) & 0x0F]);
|
|
encoded.push_back(digits[byte & 0x0F]);
|
|
}
|
|
return encoded;
|
|
}
|
|
|
|
std::optional<std::string> hexDecode(const std::string &value)
|
|
{
|
|
if (value.size() % 2 != 0)
|
|
return std::nullopt;
|
|
auto decode_nibble = [](char character) -> int
|
|
{
|
|
if (character >= '0' && character <= '9')
|
|
return character - '0';
|
|
if (character >= 'a' && character <= 'f')
|
|
return 10 + (character - 'a');
|
|
if (character >= 'A' && character <= 'F')
|
|
return 10 + (character - 'A');
|
|
return -1;
|
|
};
|
|
std::string decoded;
|
|
decoded.reserve(value.size() / 2);
|
|
for (size_t index = 0; index < value.size(); index += 2)
|
|
{
|
|
const int high = decode_nibble(value[index]);
|
|
const int low = decode_nibble(value[index + 1]);
|
|
if (high < 0 || low < 0)
|
|
return std::nullopt;
|
|
decoded.push_back(static_cast<char>((high << 4) | low));
|
|
}
|
|
return decoded;
|
|
}
|
|
|
|
std::optional<std::string> protectCredentialString(const std::string &value)
|
|
{
|
|
DATA_BLOB input{};
|
|
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(value.data()));
|
|
input.cbData = static_cast<DWORD>(value.size());
|
|
DATA_BLOB output{};
|
|
if (!CryptProtectData(&input, L"Gitree Git Credential", nullptr, nullptr, nullptr,
|
|
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
|
return std::nullopt;
|
|
std::string protected_value(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
|
LocalFree(output.pbData);
|
|
return hexEncode(protected_value);
|
|
}
|
|
|
|
std::optional<std::string> unprotectCredentialString(const std::string &value)
|
|
{
|
|
const auto binary = hexDecode(value);
|
|
if (!binary)
|
|
return std::nullopt;
|
|
DATA_BLOB input{};
|
|
input.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(binary->data()));
|
|
input.cbData = static_cast<DWORD>(binary->size());
|
|
DATA_BLOB output{};
|
|
if (!CryptUnprotectData(&input, nullptr, nullptr, nullptr, nullptr,
|
|
CRYPTPROTECT_UI_FORBIDDEN, &output))
|
|
return std::nullopt;
|
|
std::string plain(reinterpret_cast<const char *>(output.pbData), output.cbData);
|
|
LocalFree(output.pbData);
|
|
return plain;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
UserData::UserData()
|
|
{
|
|
directory_ = roaming_directory() / "Identity" / "Gitree";
|
|
std::filesystem::create_directories(directory_);
|
|
load();
|
|
}
|
|
|
|
UserData::~UserData()
|
|
{
|
|
save();
|
|
}
|
|
|
|
std::string UserData::credentialScope(const std::string &remote_url)
|
|
{
|
|
const size_t scheme = remote_url.find("://");
|
|
if (scheme == std::string::npos)
|
|
return remote_url;
|
|
const size_t authority_begin = scheme + 3;
|
|
const size_t authority_end = remote_url.find_first_of("/?#", authority_begin);
|
|
return remote_url.substr(0, authority_end == std::string::npos ? remote_url.size() : authority_end);
|
|
}
|
|
|
|
std::filesystem::path UserData::dataPath() const
|
|
{
|
|
return directory_ / "user_data.ikv2b";
|
|
}
|
|
|
|
void UserData::removeLegacyFiles() const
|
|
{
|
|
std::error_code error;
|
|
for (const char *name : {"imgui.ini", "settings.ini", "history.txt", "session.txt", "user_data.ikv"})
|
|
std::filesystem::remove(directory_ / name, error);
|
|
}
|
|
|
|
void UserData::load()
|
|
{
|
|
const std::filesystem::path binary_path = dataPath();
|
|
if (ikv_node_t *root = ikvb_parse_file(binary_path.string().c_str()))
|
|
{
|
|
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
|
|
{
|
|
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
|
|
sidebar_width_ = static_cast<float>(ikv_as_float(value));
|
|
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
|
|
details_width_ = static_cast<float>(ikv_as_float(value));
|
|
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
|
|
sidebar_collapsed_ = ikv_as_bool(value) != 0;
|
|
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
|
|
pull_mode_ = static_cast<int>(ikv_as_int(value));
|
|
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
|
|
zoom_percent_ = static_cast<int>(ikv_as_int(value));
|
|
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
|
|
}
|
|
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
|
|
}
|
|
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
|
|
}
|
|
}
|
|
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(history, index));
|
|
if (path && *path)
|
|
recently_closed_.emplace_back(path);
|
|
}
|
|
}
|
|
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(recent, index));
|
|
if (path && *path)
|
|
recent_repositories_.emplace_back(path);
|
|
}
|
|
}
|
|
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
|
|
{
|
|
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
|
|
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
|
|
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
|
|
{
|
|
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(tabs, index));
|
|
open_repositories_.emplace_back(path ? path : "");
|
|
}
|
|
}
|
|
}
|
|
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
|
{
|
|
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
|
{
|
|
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
|
if (ikv_node_type(entry) != IKV_OBJECT)
|
|
continue;
|
|
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
|
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
|
if (!scope || !secret)
|
|
continue;
|
|
const char *scope_value = ikv_as_string(scope);
|
|
const char *secret_value = ikv_as_string(secret);
|
|
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
|
continue;
|
|
encrypted_credentials_[scope_value] = secret_value;
|
|
}
|
|
}
|
|
ikv_free(root);
|
|
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
|
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
|
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
|
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
|
for (float &height : sidebar_section_heights_)
|
|
height = std::clamp(height, 42.0f, 500.0f);
|
|
for (float &width : commit_table_column_widths_)
|
|
width = std::clamp(width, 0.0f, 1200.0f);
|
|
if (open_repositories_.empty())
|
|
active_repository_ = 0;
|
|
else
|
|
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
|
return;
|
|
}
|
|
|
|
const std::filesystem::path text_data_path = directory_ / "user_data.ikv";
|
|
if (ikv_node_t *root = ikv_parse_file(text_data_path.string().c_str()))
|
|
{
|
|
if (const ikv_node_t *settings = object_value(root, "settings", IKV_OBJECT))
|
|
{
|
|
if (const ikv_node_t *value = object_value(settings, "sidebar_width", IKV_FLOAT))
|
|
sidebar_width_ = static_cast<float>(ikv_as_float(value));
|
|
if (const ikv_node_t *value = object_value(settings, "details_width", IKV_FLOAT))
|
|
details_width_ = static_cast<float>(ikv_as_float(value));
|
|
if (const ikv_node_t *value = object_value(settings, "sidebar_collapsed", IKV_BOOL))
|
|
sidebar_collapsed_ = ikv_as_bool(value) != 0;
|
|
if (const ikv_node_t *value = object_value(settings, "pull_mode", IKV_INT))
|
|
pull_mode_ = static_cast<int>(ikv_as_int(value));
|
|
if (const ikv_node_t *value = object_value(settings, "zoom_percent", IKV_INT))
|
|
zoom_percent_ = static_cast<int>(ikv_as_int(value));
|
|
if (const ikv_node_t *heights = object_value(settings, "sidebar_sections", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(heights), static_cast<uint32_t>(sidebar_section_heights_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
sidebar_section_heights_[index] = static_cast<float>(ikv_as_float(ikv_array_get(heights, index)));
|
|
}
|
|
if (const ikv_node_t *open = object_value(settings, "sidebar_section_open", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(open), static_cast<uint32_t>(sidebar_section_open_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
sidebar_section_open_[index] = ikv_as_bool(ikv_array_get(open, index)) != 0;
|
|
}
|
|
if (const ikv_node_t *widths = object_value(settings, "commit_table_columns", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(
|
|
ikv_array_size(widths), static_cast<uint32_t>(commit_table_column_widths_.size()));
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
commit_table_column_widths_[index] = static_cast<float>(ikv_as_float(ikv_array_get(widths, index)));
|
|
}
|
|
}
|
|
if (const ikv_node_t *history = object_value(root, "recently_closed", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(ikv_array_size(history), 100);
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(history, index));
|
|
if (path && *path)
|
|
recently_closed_.emplace_back(path);
|
|
}
|
|
}
|
|
if (const ikv_node_t *recent = object_value(root, "recent_repositories", IKV_ARRAY))
|
|
{
|
|
const uint32_t count = std::min<uint32_t>(ikv_array_size(recent), 100);
|
|
for (uint32_t index = 0; index < count; ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(recent, index));
|
|
if (path && *path)
|
|
recent_repositories_.emplace_back(path);
|
|
}
|
|
}
|
|
if (const ikv_node_t *session = object_value(root, "session", IKV_OBJECT))
|
|
{
|
|
if (const ikv_node_t *active = object_value(session, "active_tab", IKV_INT))
|
|
active_repository_ = static_cast<size_t>(std::max<int64_t>(0, ikv_as_int(active)));
|
|
if (const ikv_node_t *tabs = object_value(session, "tabs", IKV_ARRAY))
|
|
{
|
|
for (uint32_t index = 0; index < ikv_array_size(tabs); ++index)
|
|
{
|
|
const char *path = ikv_as_string(ikv_array_get(tabs, index));
|
|
open_repositories_.emplace_back(path ? path : "");
|
|
}
|
|
}
|
|
}
|
|
if (const ikv_node_t *credentials = object_value(root, "credentials", IKV_ARRAY))
|
|
{
|
|
for (uint32_t index = 0; index < ikv_array_size(credentials); ++index)
|
|
{
|
|
const ikv_node_t *entry = ikv_array_get(credentials, index);
|
|
if (ikv_node_type(entry) != IKV_OBJECT)
|
|
continue;
|
|
const ikv_node_t *scope = object_value(entry, "scope", IKV_STRING);
|
|
const ikv_node_t *secret = object_value(entry, "secret", IKV_STRING);
|
|
if (!scope || !secret)
|
|
continue;
|
|
const char *scope_value = ikv_as_string(scope);
|
|
const char *secret_value = ikv_as_string(secret);
|
|
if (!scope_value || !secret_value || !*scope_value || !*secret_value)
|
|
continue;
|
|
encrypted_credentials_[scope_value] = secret_value;
|
|
}
|
|
}
|
|
ikv_free(root);
|
|
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
|
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
|
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
|
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
|
for (float &height : sidebar_section_heights_)
|
|
height = std::clamp(height, 42.0f, 500.0f);
|
|
for (float &width : commit_table_column_widths_)
|
|
width = std::clamp(width, 0.0f, 1200.0f);
|
|
if (open_repositories_.empty())
|
|
active_repository_ = 0;
|
|
else
|
|
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
|
save();
|
|
return;
|
|
}
|
|
|
|
// Import the original files once when upgrading an existing installation.
|
|
std::ifstream settings(directory_ / "settings.ini");
|
|
std::string key;
|
|
while (settings >> key)
|
|
{
|
|
if (key == "sidebar_width")
|
|
settings >> sidebar_width_;
|
|
else if (key == "details_width")
|
|
settings >> details_width_;
|
|
else if (key == "pull_mode")
|
|
settings >> pull_mode_;
|
|
else if (key == "zoom_percent")
|
|
settings >> zoom_percent_;
|
|
else if (key.rfind("sidebar_section_", 0) == 0)
|
|
{
|
|
const size_t index = static_cast<size_t>(std::stoul(key.substr(16)));
|
|
float height = 0.0f;
|
|
settings >> height;
|
|
if (index < sidebar_section_heights_.size())
|
|
sidebar_section_heights_[index] = height;
|
|
}
|
|
}
|
|
sidebar_width_ = std::clamp(sidebar_width_, 180.0f, 520.0f);
|
|
details_width_ = std::clamp(details_width_, 280.0f, 650.0f);
|
|
pull_mode_ = std::clamp(pull_mode_, 0, 3);
|
|
zoom_percent_ = std::clamp(zoom_percent_, 80, 200);
|
|
for (float &height : sidebar_section_heights_)
|
|
height = std::clamp(height, 42.0f, 500.0f);
|
|
|
|
std::ifstream history(directory_ / "history.txt");
|
|
std::string path;
|
|
while (history >> std::quoted(path))
|
|
{
|
|
if (!path.empty())
|
|
recently_closed_.push_back(path);
|
|
if (recently_closed_.size() == 100)
|
|
break;
|
|
}
|
|
recent_repositories_ = recently_closed_;
|
|
|
|
std::ifstream session(directory_ / "session.txt");
|
|
session >> active_repository_;
|
|
while (session >> std::quoted(path))
|
|
open_repositories_.push_back(path);
|
|
if (open_repositories_.empty())
|
|
active_repository_ = 0;
|
|
else
|
|
active_repository_ = std::min(active_repository_, open_repositories_.size() - 1);
|
|
save();
|
|
}
|
|
|
|
std::optional<SavedGitCredential> UserData::remoteCredential(const std::string &remote_url) const
|
|
{
|
|
const auto iterator = encrypted_credentials_.find(credentialScope(remote_url));
|
|
if (iterator == encrypted_credentials_.end())
|
|
return std::nullopt;
|
|
#ifdef _WIN32
|
|
const auto plain = unprotectCredentialString(iterator->second);
|
|
if (!plain)
|
|
return std::nullopt;
|
|
const auto split = splitCredentialPayload(*plain);
|
|
if (!split)
|
|
return std::nullopt;
|
|
return SavedGitCredential{remote_url, split->first, split->second};
|
|
#else
|
|
return std::nullopt;
|
|
#endif
|
|
}
|
|
|
|
void UserData::addRecentRepository(const std::string &path)
|
|
{
|
|
if (path.empty())
|
|
return;
|
|
std::erase(recent_repositories_, path);
|
|
recent_repositories_.insert(recent_repositories_.begin(), path);
|
|
if (recent_repositories_.size() > 100)
|
|
recent_repositories_.resize(100);
|
|
save();
|
|
}
|
|
|
|
void UserData::addRecentlyClosed(const std::string &path)
|
|
{
|
|
if (path.empty())
|
|
return;
|
|
std::erase(recent_repositories_, path);
|
|
recent_repositories_.insert(recent_repositories_.begin(), path);
|
|
if (recent_repositories_.size() > 100)
|
|
recent_repositories_.resize(100);
|
|
std::erase(recently_closed_, path);
|
|
recently_closed_.insert(recently_closed_.begin(), path);
|
|
if (recently_closed_.size() > 100)
|
|
recently_closed_.resize(100);
|
|
save();
|
|
}
|
|
|
|
std::string UserData::takeRecentlyClosed()
|
|
{
|
|
if (recently_closed_.empty())
|
|
return {};
|
|
std::string path = std::move(recently_closed_.front());
|
|
recently_closed_.erase(recently_closed_.begin());
|
|
save();
|
|
return path;
|
|
}
|
|
|
|
void UserData::setRepositorySession(std::vector<std::string> paths, size_t active_repository)
|
|
{
|
|
open_repositories_ = std::move(paths);
|
|
active_repository_ = open_repositories_.empty()
|
|
? 0
|
|
: std::min(active_repository, open_repositories_.size() - 1);
|
|
save();
|
|
}
|
|
|
|
void UserData::storeRemoteCredential(const std::string &remote_url, const std::string &username,
|
|
const std::string &password)
|
|
{
|
|
const std::string scope = credentialScope(remote_url);
|
|
if (scope.empty() || username.empty())
|
|
return;
|
|
#ifdef _WIN32
|
|
const auto protected_value = protectCredentialString(username + "\n" + password);
|
|
if (!protected_value)
|
|
return;
|
|
encrypted_credentials_[scope] = *protected_value;
|
|
save();
|
|
#else
|
|
(void)password;
|
|
#endif
|
|
}
|
|
|
|
void UserData::clearRemoteCredential(const std::string &remote_url)
|
|
{
|
|
encrypted_credentials_.erase(credentialScope(remote_url));
|
|
save();
|
|
}
|
|
|
|
void UserData::save() const
|
|
{
|
|
std::filesystem::create_directories(directory_);
|
|
ikv_node_t *root = ikv_create_object("gitree");
|
|
if (!root)
|
|
return;
|
|
|
|
ikv_node_t *settings = ikv_object_add_object(root, "settings");
|
|
ikv_object_set_float(settings, "sidebar_width", sidebar_width_);
|
|
ikv_object_set_float(settings, "details_width", details_width_);
|
|
ikv_object_set_bool(settings, "sidebar_collapsed", sidebar_collapsed_);
|
|
ikv_object_set_int(settings, "pull_mode", pull_mode_);
|
|
ikv_object_set_int(settings, "zoom_percent", zoom_percent_);
|
|
ikv_node_t *heights = ikv_object_add_array(settings, "sidebar_sections", IKV_FLOAT);
|
|
for (const float height : sidebar_section_heights_)
|
|
ikv_array_add_float(heights, height);
|
|
ikv_node_t *open = ikv_object_add_array(settings, "sidebar_section_open", IKV_BOOL);
|
|
for (const bool state : sidebar_section_open_)
|
|
ikv_array_add_bool(open, state);
|
|
ikv_node_t *widths = ikv_object_add_array(settings, "commit_table_columns", IKV_FLOAT);
|
|
for (const float width : commit_table_column_widths_)
|
|
ikv_array_add_float(widths, width);
|
|
|
|
ikv_node_t *history = ikv_object_add_array(root, "recently_closed", IKV_STRING);
|
|
for (const auto &path : recently_closed_)
|
|
ikv_array_add_string(history, path.c_str());
|
|
|
|
ikv_node_t *recent = ikv_object_add_array(root, "recent_repositories", IKV_STRING);
|
|
for (const auto &path : recent_repositories_)
|
|
ikv_array_add_string(recent, path.c_str());
|
|
|
|
ikv_node_t *session = ikv_object_add_object(root, "session");
|
|
ikv_object_set_int(session, "active_tab", static_cast<int64_t>(active_repository_));
|
|
ikv_node_t *tabs = ikv_object_add_array(session, "tabs", IKV_STRING);
|
|
for (const auto &path : open_repositories_)
|
|
ikv_array_add_string(tabs, path.c_str());
|
|
|
|
ikv_node_t *credentials = ikv_object_add_array(root, "credentials", IKV_OBJECT);
|
|
for (const auto &[scope, secret] : encrypted_credentials_)
|
|
{
|
|
ikv_node_t *entry = ikv_array_add_object(credentials);
|
|
ikv_object_set_string(entry, "scope", scope.c_str());
|
|
ikv_object_set_string(entry, "secret", secret.c_str());
|
|
}
|
|
|
|
ikvb_write_file_version(dataPath().string().c_str(), root, IKV_VERSION_2);
|
|
ikv_free(root);
|
|
removeLegacyFiles();
|
|
}
|