Configuration
This commit is contained in:
@@ -4,6 +4,7 @@ project(Dawn
|
||||
DESCRIPTION "Colour-managed image browser"
|
||||
LANGUAGES C CXX
|
||||
)
|
||||
set(DAWN_NAMESPACE "name.janouch.dawn")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#pragma once
|
||||
|
||||
#define DAWN_NAME "@PROJECT_NAME@"
|
||||
#define DAWN_NAMESPACE "@DAWN_NAMESPACE@"
|
||||
#define DAWN_VERSION "@PROJECT_VERSION@"
|
||||
|
||||
#cmakedefine01 DAWN_WITH_LCMS2_FAST_FLOAT
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ if (APPLE)
|
||||
set_target_properties(dn PROPERTIES
|
||||
RUNTIME_OUTPUT_NAME "${PROJECT_NAME}"
|
||||
MACOSX_BUNDLE ON
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER name.janouch.dn
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER "${DAWN_NAMESPACE}"
|
||||
MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}"
|
||||
MACOSX_BUNDLE_INFO_PLIST "${plist}"
|
||||
MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}"
|
||||
|
||||
+119
-6
@@ -21,6 +21,7 @@
|
||||
#include <QByteArray>
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFileOpenEvent>
|
||||
#include <QGuiApplication>
|
||||
@@ -30,8 +31,11 @@
|
||||
#include <QtLogging>
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
#include "app-menu-macos.hpp"
|
||||
@@ -44,6 +48,49 @@ using namespace std;
|
||||
namespace dn
|
||||
{
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
constexpr string_view kBookmarksKey = "dn/Bookmarks";
|
||||
|
||||
optional<string>
|
||||
setting(string_view key)
|
||||
{
|
||||
dawn::Error error;
|
||||
optional<string> value = dawn::config_get(key, &error);
|
||||
if (error)
|
||||
qWarning("configuration %.*s: %s", int(key.size()), key.data(),
|
||||
error.message.c_str());
|
||||
return value;
|
||||
}
|
||||
|
||||
bool
|
||||
boolean_setting(string_view key, bool fallback)
|
||||
{
|
||||
const optional<string> value = setting(key);
|
||||
if (!value)
|
||||
return fallback;
|
||||
if (*value == "true" || *value == "1")
|
||||
return true;
|
||||
if (*value == "false" || *value == "0")
|
||||
return false;
|
||||
qWarning("configuration %.*s: expected true, false, 1, or 0",
|
||||
int(key.size()), key.data());
|
||||
return fallback;
|
||||
}
|
||||
|
||||
char
|
||||
bookmark_separator()
|
||||
{
|
||||
#ifdef Q_OS_WIN
|
||||
return ';';
|
||||
#else
|
||||
return ':';
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Bookmarks are compared by path, so they are stored canonicalised: the
|
||||
// sidebar highlights the open directory by std::filesystem::equivalent,
|
||||
// and a trailing separator or an unresolved symlink must not disagree.
|
||||
@@ -63,12 +110,66 @@ canonical_dir(const string &path)
|
||||
return p.string();
|
||||
}
|
||||
|
||||
void
|
||||
Settings::load()
|
||||
{
|
||||
this->disable_dithering =
|
||||
boolean_setting("dn/DisableDithering", false);
|
||||
this->browser_show_filenames =
|
||||
boolean_setting("dn/BrowserShowFilenames", true);
|
||||
if (const optional<string> value = setting("dn/BrowserThumbnailSize")) {
|
||||
int size = 0;
|
||||
const auto parsed = from_chars(
|
||||
value->data(), value->data() + value->size(), size);
|
||||
if (parsed.ec == errc{} && parsed.ptr == value->data() + value->size() &&
|
||||
(size == 128 || size == 256 || size == 512 || size == 1024)) {
|
||||
this->browser_thumbnail_size = size;
|
||||
} else {
|
||||
qWarning("configuration dn/BrowserThumbnailSize: unsupported size");
|
||||
}
|
||||
}
|
||||
|
||||
this->bookmarks.clear();
|
||||
if (const optional<string> value = setting(kBookmarksKey)) {
|
||||
const char separator = bookmark_separator();
|
||||
for (size_t offset = 0; offset <= value->size();) {
|
||||
const size_t end = value->find(separator, offset);
|
||||
const string item = value->substr(offset,
|
||||
end == string::npos ? string::npos : end - offset);
|
||||
if (!item.empty())
|
||||
this->bookmarks.push_back(item);
|
||||
if (end == string::npos)
|
||||
break;
|
||||
offset = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
this->icc_profile_override.clear();
|
||||
this->icc_profile_override_path.clear();
|
||||
if (const optional<string> value = setting("dn/ICCProfileOverride");
|
||||
value && !value->empty()) {
|
||||
QFile file(QString::fromUtf8(value->data(), qsizetype(value->size())));
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qWarning("configuration dn/ICCProfileOverride: cannot open %s",
|
||||
value->c_str());
|
||||
} else {
|
||||
const QByteArray bytes = file.readAll();
|
||||
if (bytes.isEmpty()) {
|
||||
qWarning("configuration dn/ICCProfileOverride: empty ICC profile");
|
||||
} else {
|
||||
this->icc_profile_override.assign(bytes.begin(), bytes.end());
|
||||
this->icc_profile_override_path = *value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
Settings::bookmarked(const string &path) const
|
||||
{
|
||||
const string want = canonical_dir(path);
|
||||
return find(this->bookmarks_.begin(), this->bookmarks_.end(), want) !=
|
||||
this->bookmarks_.end();
|
||||
return find(this->bookmarks.begin(), this->bookmarks.end(), want) !=
|
||||
this->bookmarks.end();
|
||||
}
|
||||
|
||||
void
|
||||
@@ -76,12 +177,23 @@ Settings::toggle_bookmark(const string &path)
|
||||
{
|
||||
const string want = canonical_dir(path);
|
||||
const auto it =
|
||||
find(this->bookmarks_.begin(), this->bookmarks_.end(), want);
|
||||
if (it != this->bookmarks_.end())
|
||||
this->bookmarks_.erase(it);
|
||||
find(this->bookmarks.begin(), this->bookmarks.end(), want);
|
||||
if (it != this->bookmarks.end())
|
||||
this->bookmarks.erase(it);
|
||||
else
|
||||
this->bookmarks_.push_back(want);
|
||||
this->bookmarks.push_back(want);
|
||||
notify();
|
||||
|
||||
string value;
|
||||
for (const string &bookmark : this->bookmarks) {
|
||||
if (!value.empty())
|
||||
value += bookmark_separator();
|
||||
value += bookmark;
|
||||
}
|
||||
dawn::Error error;
|
||||
if (!dawn::config_set(kBookmarksKey, value, &error))
|
||||
qWarning("configuration %.*s: %s", int(kBookmarksKey.size()),
|
||||
kBookmarksKey.data(), error.message.c_str());
|
||||
}
|
||||
|
||||
void
|
||||
@@ -123,6 +235,7 @@ App::event(QEvent *event)
|
||||
bool
|
||||
App::init()
|
||||
{
|
||||
this->settings.load();
|
||||
#if DN_WITH_WAYLAND
|
||||
// Vulkan content is a wl_subsurface. Qt's presentAboutToBeQueued waits on
|
||||
// wl_surface.frame and marks the window unexposed on timeout; that
|
||||
|
||||
+8
-5
@@ -44,16 +44,19 @@ enum class OpenResult : uint8_t {
|
||||
/// Notification is coarse: any change notifies all listeners.
|
||||
class Settings
|
||||
{
|
||||
std::vector<std::string> bookmarks_;
|
||||
std::vector<std::pair<void *, std::function<void()>>> listeners_;
|
||||
|
||||
void notify() const;
|
||||
|
||||
public:
|
||||
[[nodiscard]] const std::vector<std::string> &bookmarks() const
|
||||
{
|
||||
return this->bookmarks_;
|
||||
}
|
||||
std::vector<std::string> bookmarks;
|
||||
std::vector<unsigned char> icc_profile_override;
|
||||
std::string icc_profile_override_path;
|
||||
bool disable_dithering = false;
|
||||
bool browser_show_filenames = true;
|
||||
int browser_thumbnail_size = 256;
|
||||
|
||||
void load();
|
||||
[[nodiscard]] bool bookmarked(const std::string &path) const;
|
||||
void toggle_bookmark(const std::string &path);
|
||||
void listen(void *key, std::function<void()> fn);
|
||||
|
||||
+89
-160
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "xdg.hpp"
|
||||
|
||||
#include <libdn.h>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
@@ -20,6 +22,8 @@
|
||||
#include <QUrl>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
@@ -36,21 +40,18 @@ namespace
|
||||
// It should actually be derived from the active QGuiApplication.
|
||||
constexpr auto kSelfDesktop = QLatin1String("dn.desktop");
|
||||
|
||||
QString
|
||||
string
|
||||
read_text_file(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
return {};
|
||||
|
||||
QString text = QString::fromUtf8(file.readAll());
|
||||
text.replace(QLatin1String("\r\n"), QLatin1String("\n"));
|
||||
text.replace(u'\r', u'\n');
|
||||
return text;
|
||||
return file.readAll().toStdString();
|
||||
}
|
||||
|
||||
bool
|
||||
write_text_file(const QString &path, const QString &text)
|
||||
write_text_file(const QString &path, string_view text)
|
||||
{
|
||||
QFileInfo info(path);
|
||||
if (!QDir().mkpath(info.absolutePath()))
|
||||
@@ -61,49 +62,28 @@ write_text_file(const QString &path, const QString &text)
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly))
|
||||
return false;
|
||||
const QByteArray data = text.toUtf8();
|
||||
if (file.write(data) != data.size())
|
||||
if (file.write(text.data(), qsizetype(text.size())) !=
|
||||
qsizetype(text.size()))
|
||||
return false;
|
||||
return file.commit();
|
||||
}
|
||||
|
||||
QString
|
||||
unescape_desktop(const QString &value)
|
||||
{
|
||||
QString out;
|
||||
out.reserve(value.size());
|
||||
for (int i = 0; i < value.size(); ++i) {
|
||||
if (value[i] == u'\\' && i + 1 < value.size()) {
|
||||
const QChar n = value[++i];
|
||||
if (n == u's')
|
||||
out += u' ';
|
||||
else if (n == u'n')
|
||||
out += u'\n';
|
||||
else if (n == u't')
|
||||
out += u'\t';
|
||||
else if (n == u'r')
|
||||
out += u'\r';
|
||||
else
|
||||
out += n;
|
||||
} else {
|
||||
out += value[i];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool
|
||||
parse_bool(const QString &value)
|
||||
parse_bool(string_view value)
|
||||
{
|
||||
const QString v = value.trimmed().toLower();
|
||||
const QString v = QString::fromUtf8(value.data(), qsizetype(value.size()))
|
||||
.trimmed()
|
||||
.toLower();
|
||||
return v == QLatin1String("true") || v == QLatin1String("1");
|
||||
}
|
||||
|
||||
vector<QString>
|
||||
split_semicolons(const QString &value)
|
||||
split_semicolons(string_view value)
|
||||
{
|
||||
vector<QString> out;
|
||||
for (const QString &part : value.split(u';', Qt::SkipEmptyParts)) {
|
||||
const QString decoded =
|
||||
QString::fromUtf8(value.data(), qsizetype(value.size()));
|
||||
for (const QString &part : decoded.split(u';', Qt::SkipEmptyParts)) {
|
||||
const QString item = part.trimmed();
|
||||
if (!item.isEmpty())
|
||||
out.push_back(item);
|
||||
@@ -117,6 +97,7 @@ normalize_desktop_id(QString id)
|
||||
id = id.trimmed();
|
||||
if (id.isEmpty())
|
||||
return {};
|
||||
|
||||
if (!id.endsWith(QLatin1String(".desktop")))
|
||||
id += QLatin1String(".desktop");
|
||||
return id;
|
||||
@@ -144,8 +125,7 @@ locale_candidates()
|
||||
for (const QString &part : language.split(u':', Qt::SkipEmptyParts))
|
||||
raw.push_back(part);
|
||||
}
|
||||
// QLocale::system() has nothing but these three to go on here, and it
|
||||
// discards the modifier, so it would only ever repeat one of them.
|
||||
|
||||
raw.push_back(qEnvironmentVariable("LC_ALL"));
|
||||
raw.push_back(qEnvironmentVariable("LC_MESSAGES"));
|
||||
raw.push_back(qEnvironmentVariable("LANG"));
|
||||
@@ -188,89 +168,8 @@ locale_candidates()
|
||||
return out;
|
||||
}
|
||||
|
||||
struct IniGroup {
|
||||
QString name;
|
||||
vector<pair<QString, QString>> keys;
|
||||
};
|
||||
|
||||
struct IniFile {
|
||||
vector<QString> preamble;
|
||||
vector<IniGroup> groups;
|
||||
};
|
||||
|
||||
IniFile
|
||||
parse_ini(const QString &text)
|
||||
{
|
||||
IniFile ini;
|
||||
IniGroup *group = nullptr;
|
||||
for (const QString &raw : text.split(u'\n')) {
|
||||
const QString trimmed = raw.trimmed();
|
||||
if (trimmed.isEmpty() || trimmed.startsWith(u'#')) {
|
||||
if (!group)
|
||||
ini.preamble.push_back(raw);
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith(u'[') && trimmed.endsWith(u']') &&
|
||||
!trimmed.contains(u'=')) {
|
||||
IniGroup g;
|
||||
g.name = trimmed.mid(1, trimmed.size() - 2);
|
||||
ini.groups.push_back(std::move(g));
|
||||
group = &ini.groups.back();
|
||||
continue;
|
||||
}
|
||||
if (!group)
|
||||
continue;
|
||||
const int eq = raw.indexOf(u'=');
|
||||
if (eq < 0)
|
||||
continue;
|
||||
group->keys.push_back({raw.left(eq).trimmed(), raw.mid(eq + 1)});
|
||||
}
|
||||
return ini;
|
||||
}
|
||||
|
||||
QString
|
||||
ini_get(const IniGroup &group, const QString &key)
|
||||
{
|
||||
for (const auto &kv : group.keys) {
|
||||
if (kv.first == key)
|
||||
return kv.second;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
ini_set(IniGroup &group, const QString &key, const QString &value)
|
||||
{
|
||||
for (auto &kv : group.keys) {
|
||||
if (kv.first == key) {
|
||||
kv.second = value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
group.keys.push_back({key, value});
|
||||
}
|
||||
|
||||
QString
|
||||
serialize_ini(const IniFile &ini)
|
||||
{
|
||||
QString out;
|
||||
for (const QString &line : ini.preamble) {
|
||||
out += line;
|
||||
out += u'\n';
|
||||
}
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
out += u'[';
|
||||
out += group.name;
|
||||
out += QLatin1String("]\n");
|
||||
for (const auto &kv : group.keys) {
|
||||
out += kv.first;
|
||||
out += u'=';
|
||||
out += kv.second;
|
||||
out += u'\n';
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
using IniGroup = dawn::detail::IniGroup;
|
||||
using IniFile = dawn::detail::IniFile;
|
||||
|
||||
vector<QString>
|
||||
mimeapps_list_paths()
|
||||
@@ -313,18 +212,22 @@ append_unique(vector<QString> &list, const QString &id)
|
||||
void
|
||||
apply_mimeapps(AssocSets &acc, const IniFile &ini, const QString &type)
|
||||
{
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name == QLatin1String("Default Applications")) {
|
||||
for (const QString &id : split_semicolons(ini_get(group, type)))
|
||||
if (group.name == "Default Applications") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8)))
|
||||
append_unique(acc.defaults, normalize_desktop_id(id));
|
||||
} else if (group.name == QLatin1String("Added Associations")) {
|
||||
for (const QString &id : split_semicolons(ini_get(group, type))) {
|
||||
} else if (group.name == "Added Associations") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(id);
|
||||
if (!acc.removed.contains(nid))
|
||||
append_unique(acc.added, nid);
|
||||
}
|
||||
} else if (group.name == QLatin1String("Removed Associations")) {
|
||||
for (const QString &id : split_semicolons(ini_get(group, type))) {
|
||||
} else if (group.name == "Removed Associations") {
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(id);
|
||||
if (find(acc.added.begin(), acc.added.end(), nid) ==
|
||||
acc.added.end())
|
||||
@@ -341,7 +244,8 @@ associations_for_type(const QString &type)
|
||||
for (const QString &path : mimeapps_list_paths()) {
|
||||
if (!QFileInfo::exists(path))
|
||||
continue;
|
||||
apply_mimeapps(acc, parse_ini(read_text_file(path)), type);
|
||||
apply_mimeapps(
|
||||
acc, dawn::detail::ini_parse(read_text_file(path)), type);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
@@ -355,11 +259,14 @@ cache_ids_for_type(const QString &type)
|
||||
QDir(dir).filePath(QStringLiteral("applications/mimeinfo.cache"));
|
||||
if (!QFileInfo::exists(path))
|
||||
continue;
|
||||
const IniFile ini = parse_ini(read_text_file(path));
|
||||
|
||||
const IniFile ini = dawn::detail::ini_parse(read_text_file(path));
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name != QLatin1String("MIME Cache"))
|
||||
if (group.name != "MIME Cache")
|
||||
continue;
|
||||
for (const QString &id : split_semicolons(ini_get(group, type)))
|
||||
for (const QString &id : split_semicolons(
|
||||
dawn::detail::ini_get(group, type_utf8)))
|
||||
append_unique(ids, normalize_desktop_id(id));
|
||||
}
|
||||
}
|
||||
@@ -409,16 +316,19 @@ localized_value(const IniGroup &entry, const QString &key)
|
||||
unordered_map<QString, QString> localized;
|
||||
QString fallback;
|
||||
for (const auto &kv : entry.keys) {
|
||||
if (kv.first == key) {
|
||||
const QString item_key = QString::fromStdString(kv.first);
|
||||
if (item_key == key) {
|
||||
if (fallback.isEmpty())
|
||||
fallback = unescape_desktop(kv.second);
|
||||
fallback = QString::fromStdString(
|
||||
dawn::detail::desktop_unescape(kv.second));
|
||||
continue;
|
||||
}
|
||||
if (!kv.first.startsWith(prefix) || !kv.first.endsWith(u']'))
|
||||
if (!item_key.startsWith(prefix) || !item_key.endsWith(u']'))
|
||||
continue;
|
||||
const QString loc =
|
||||
kv.first.mid(prefix.size(), kv.first.size() - prefix.size() - 1);
|
||||
localized.insert({loc, unescape_desktop(kv.second)});
|
||||
const QString loc = item_key.mid(
|
||||
prefix.size(), item_key.size() - prefix.size() - 1);
|
||||
localized.insert({loc, QString::fromStdString(
|
||||
dawn::detail::desktop_unescape(kv.second))});
|
||||
}
|
||||
for (const QString &loc : locale_candidates()) {
|
||||
const auto it = localized.find(loc);
|
||||
@@ -446,9 +356,11 @@ try_exec_ok(const QString &try_exec)
|
||||
{
|
||||
if (try_exec.isEmpty())
|
||||
return true;
|
||||
|
||||
const QFileInfo info(try_exec);
|
||||
if (info.isAbsolute())
|
||||
return info.isFile() && info.isExecutable();
|
||||
|
||||
const QString path = qEnvironmentVariable("PATH");
|
||||
for (const QString &dir : path.split(u':', Qt::SkipEmptyParts)) {
|
||||
const QFileInfo cand(QDir(dir).filePath(try_exec));
|
||||
@@ -490,27 +402,34 @@ load_desktop(const QString &id)
|
||||
d.path = desktop_path_for_id(id);
|
||||
if (d.path.isEmpty())
|
||||
return d;
|
||||
const IniFile ini = parse_ini(read_text_file(d.path));
|
||||
|
||||
const IniFile ini = dawn::detail::ini_parse(read_text_file(d.path));
|
||||
const IniGroup *entry = nullptr;
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
if (group.name == QLatin1String("Desktop Entry")) {
|
||||
if (group.name == "Desktop Entry") {
|
||||
entry = &group;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!entry)
|
||||
return d;
|
||||
const QString type = ini_get(*entry, QStringLiteral("Type")).trimmed();
|
||||
|
||||
const QString type = QString::fromStdString(
|
||||
dawn::detail::ini_get(*entry, "Type"))
|
||||
.trimmed();
|
||||
d.application = type.isEmpty() || type == QLatin1String("Application");
|
||||
d.name = localized_name(*entry);
|
||||
d.icon = unescape_desktop(ini_get(*entry, QStringLiteral("Icon")));
|
||||
d.exec = unescape_desktop(ini_get(*entry, QStringLiteral("Exec")));
|
||||
d.try_exec = unescape_desktop(ini_get(*entry, QStringLiteral("TryExec")));
|
||||
d.hidden = parse_bool(ini_get(*entry, QStringLiteral("Hidden")));
|
||||
d.icon = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "Icon")));
|
||||
d.exec = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "Exec")));
|
||||
d.try_exec = QString::fromStdString(dawn::detail::desktop_unescape(
|
||||
dawn::detail::ini_get(*entry, "TryExec")));
|
||||
d.hidden = parse_bool(dawn::detail::ini_get(*entry, "Hidden"));
|
||||
d.only_show_in =
|
||||
split_semicolons(ini_get(*entry, QStringLiteral("OnlyShowIn")));
|
||||
split_semicolons(dawn::detail::ini_get(*entry, "OnlyShowIn"));
|
||||
d.not_show_in =
|
||||
split_semicolons(ini_get(*entry, QStringLiteral("NotShowIn")));
|
||||
split_semicolons(dawn::detail::ini_get(*entry, "NotShowIn"));
|
||||
return d;
|
||||
}
|
||||
|
||||
@@ -720,7 +639,8 @@ prepend_id(const QString &value, const QString &id)
|
||||
{
|
||||
vector<QString> ids;
|
||||
append_unique(ids, id);
|
||||
for (const QString &existing : split_semicolons(value))
|
||||
for (const QString &existing :
|
||||
split_semicolons(value.toUtf8().toStdString()))
|
||||
append_unique(ids, normalize_desktop_id(existing));
|
||||
QString out;
|
||||
for (const QString &item : ids) {
|
||||
@@ -812,6 +732,7 @@ launch(const Handler &app, const QString &path)
|
||||
{
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return false;
|
||||
|
||||
const Desktop *d = desktop_by_id(app.id);
|
||||
if (!d || d->exec.isEmpty())
|
||||
return false;
|
||||
@@ -826,6 +747,7 @@ set_last_used(const Handler &app, const QString &path)
|
||||
{
|
||||
if (app.id.isEmpty() || path.isEmpty())
|
||||
return;
|
||||
|
||||
const QString id = normalize_desktop_id(app.id);
|
||||
if (id.isEmpty() || id == kSelfDesktop)
|
||||
return;
|
||||
@@ -836,24 +758,26 @@ set_last_used(const Handler &app, const QString &path)
|
||||
if (dest.isEmpty())
|
||||
return;
|
||||
|
||||
IniFile ini = parse_ini(read_text_file(dest));
|
||||
auto find_group = [&](const QLatin1String name) -> IniGroup * {
|
||||
IniFile ini = dawn::detail::ini_parse(read_text_file(dest));
|
||||
auto find_group = [&](string_view name) -> IniGroup * {
|
||||
for (IniGroup &group : ini.groups) {
|
||||
if (group.name == name)
|
||||
return &group;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
if (!find_group(QLatin1String("Added Associations"))) {
|
||||
if (!find_group("Added Associations")) {
|
||||
IniGroup group;
|
||||
group.name = QStringLiteral("Added Associations");
|
||||
group.name = "Added Associations";
|
||||
ini.groups.push_back(std::move(group));
|
||||
}
|
||||
IniGroup *added = find_group(QLatin1String("Added Associations"));
|
||||
IniGroup *removed = find_group(QLatin1String("Removed Associations"));
|
||||
IniGroup *added = find_group("Added Associations");
|
||||
IniGroup *removed = find_group("Removed Associations");
|
||||
auto drop_id = [&](IniGroup &group, const QString &type) {
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
vector<QString> kept;
|
||||
for (const QString &existing : split_semicolons(ini_get(group, type))) {
|
||||
for (const QString &existing :
|
||||
split_semicolons(dawn::detail::ini_get(group, type_utf8))) {
|
||||
const QString nid = normalize_desktop_id(existing);
|
||||
if (nid != id)
|
||||
append_unique(kept, nid);
|
||||
@@ -865,20 +789,25 @@ set_last_used(const Handler &app, const QString &path)
|
||||
}
|
||||
if (value.isEmpty()) {
|
||||
group.keys.erase(remove_if(group.keys.begin(), group.keys.end(),
|
||||
[&](const pair<QString, QString> &kv) {
|
||||
return kv.first == type;
|
||||
[&](const pair<string, string> &kv) {
|
||||
return kv.first == type_utf8;
|
||||
}),
|
||||
group.keys.end());
|
||||
} else {
|
||||
ini_set(group, type, value);
|
||||
dawn::detail::ini_set(
|
||||
group, type_utf8, value.toUtf8().toStdString());
|
||||
}
|
||||
};
|
||||
for (const QString &type : types) {
|
||||
ini_set(*added, type, prepend_id(ini_get(*added, type), id));
|
||||
const string type_utf8 = type.toUtf8().toStdString();
|
||||
const QString previous = QString::fromStdString(
|
||||
dawn::detail::ini_get(*added, type_utf8));
|
||||
dawn::detail::ini_set(*added, type_utf8,
|
||||
prepend_id(previous, id).toUtf8().toStdString());
|
||||
if (removed)
|
||||
drop_id(*removed, type);
|
||||
}
|
||||
write_text_file(dest, serialize_ini(ini));
|
||||
write_text_file(dest, dawn::detail::ini_serialize(ini));
|
||||
}
|
||||
|
||||
} // namespace dn
|
||||
|
||||
+2
-3
@@ -268,8 +268,7 @@ Renderer::destroy()
|
||||
bool
|
||||
Renderer::dithering() const
|
||||
{
|
||||
// TODO(p): User should be able to disable that.
|
||||
return is_unorm8(this->format_);
|
||||
return this->dither_enabled_ && is_unorm8(this->format_);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -326,7 +325,7 @@ Renderer::create_swapchain()
|
||||
qWarning("swapchain: PASS_THROUGH unavailable; "
|
||||
"using compositor-managed sRGB");
|
||||
}
|
||||
const bool dither = is_unorm8(this->format_);
|
||||
const bool dither = dithering();
|
||||
const VkFormat dest_format =
|
||||
dither ? VK_FORMAT_R16G16B16A16_UNORM : this->format_;
|
||||
const VkImageLayout dest_layout = dither
|
||||
|
||||
@@ -89,6 +89,7 @@ class Renderer
|
||||
VkShaderModule dither_frag_ = VK_NULL_HANDLE;
|
||||
bool needs_resize_ = false;
|
||||
bool prefer_premultiplied_ = false;
|
||||
bool dither_enabled_ = true;
|
||||
uint32_t dest_inset_ = 0;
|
||||
std::function<void()> present_about_to_queue_;
|
||||
std::function<void()> present_queued_;
|
||||
@@ -111,6 +112,7 @@ public:
|
||||
dawn::Orientation orientation, float angle = 0.f);
|
||||
void set_well_colour(float r, float g, float b);
|
||||
void set_prefer_premultiplied(bool enabled);
|
||||
void set_dither_enabled(bool enabled) { this->dither_enabled_ = enabled; }
|
||||
void set_dest_inset(uint32_t px);
|
||||
void set_checker_colour(float r, float g, float b);
|
||||
void set_checkerboard(bool enabled);
|
||||
|
||||
+27
-8
@@ -225,6 +225,8 @@ Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
|
||||
// TODO: Pass an explicit presentation policy from WaylandWindow instead of
|
||||
// using parenthood as this platform/role proxy.
|
||||
this->renderer_.set_prefer_premultiplied(this->csd_);
|
||||
this->renderer_.set_dither_enabled(
|
||||
!this->app_->settings.disable_dithering);
|
||||
if (!this->renderer_.init(
|
||||
this->app_->gpu, this->surface_, pixel_size(),
|
||||
parent() ? VK_PRESENT_MODE_MAILBOX_KHR : VK_PRESENT_MODE_FIFO_KHR,
|
||||
@@ -254,6 +256,10 @@ Window::initialize(const QUrl &url, BrowseSetup setup, bool browse)
|
||||
if (this->browser_) {
|
||||
this->browser_->set_screen_profile(this->cmm_, this->screen_profile_);
|
||||
this->browser_->setup_ = setup;
|
||||
this->browser_->show_names_ =
|
||||
this->app_->settings.browser_show_filenames;
|
||||
this->browser_->thumb_size_ =
|
||||
this->app_->settings.browser_thumbnail_size;
|
||||
}
|
||||
|
||||
open_any(url.isEmpty() ? path_to_url(QDir::currentPath()) : url, browse);
|
||||
@@ -385,7 +391,7 @@ Window::bind_host()
|
||||
};
|
||||
this->host_.launch_exiftool = [this](QUrl url) { launch_exiftool(url); };
|
||||
this->host_.trash = [this](QUrl url) { trash_url(url); };
|
||||
this->host_.bookmarks = [this] { return this->app_->settings.bookmarks(); };
|
||||
this->host_.bookmarks = [this] { return this->app_->settings.bookmarks; };
|
||||
this->host_.bookmarked = [this](const QUrl &url) {
|
||||
return this->app_->settings.bookmarked(url_to_path(url).toStdString());
|
||||
};
|
||||
@@ -614,17 +620,30 @@ Window::refresh_screen_profile(QScreen *target_screen)
|
||||
if (!this->cmm_)
|
||||
this->cmm_ = dawn::Cmm::get_default();
|
||||
|
||||
DisplayProfile discovered =
|
||||
this->app_->display_profiles.load(target_screen);
|
||||
shared_ptr<dawn::Profile> next;
|
||||
string label = "sRGB (fallback)";
|
||||
string source = "srgb";
|
||||
if (!discovered.icc.empty()) {
|
||||
next = this->cmm_->get_profile(discovered.icc);
|
||||
const vector<unsigned char> &override =
|
||||
this->app_->settings.icc_profile_override;
|
||||
if (!override.empty()) {
|
||||
next = this->cmm_->get_profile(override);
|
||||
if (next) {
|
||||
label =
|
||||
discovered.label.empty() ? discovered.source : discovered.label;
|
||||
source = discovered.source;
|
||||
label = this->app_->settings.icc_profile_override_path;
|
||||
source = "configuration";
|
||||
} else {
|
||||
qWarning("configuration dn/ICCProfileOverride: invalid ICC profile");
|
||||
}
|
||||
}
|
||||
if (!next) {
|
||||
DisplayProfile discovered =
|
||||
this->app_->display_profiles.load(target_screen);
|
||||
if (!discovered.icc.empty()) {
|
||||
next = this->cmm_->get_profile(discovered.icc);
|
||||
if (next) {
|
||||
label = discovered.label.empty() ? discovered.source
|
||||
: discovered.label;
|
||||
source = discovered.source;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!next)
|
||||
|
||||
@@ -79,10 +79,17 @@ endif()
|
||||
add_library(dawn::libdn ALIAS libdn)
|
||||
|
||||
if (WIN32)
|
||||
target_sources(libdn PRIVATE ipc-windows.cpp ipc-rpc.cpp ipc-instance.cpp)
|
||||
target_sources(libdn PRIVATE
|
||||
config-windows.cpp ipc-windows.cpp ipc-rpc.cpp ipc-instance.cpp)
|
||||
target_link_libraries(libdn PRIVATE advapi32)
|
||||
elseif (APPLE)
|
||||
enable_language(OBJCXX)
|
||||
find_library(DAWN_COREFOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
|
||||
target_sources(libdn PRIVATE config-macos.mm)
|
||||
target_link_libraries(libdn PRIVATE "${DAWN_COREFOUNDATION_FRAMEWORK}")
|
||||
elseif (NOT APPLE)
|
||||
target_sources(libdn PRIVATE ipc-unix.cpp ipc-rpc.cpp ipc-instance.cpp)
|
||||
target_sources(libdn PRIVATE
|
||||
config-unix.cpp ipc-unix.cpp ipc-rpc.cpp ipc-instance.cpp)
|
||||
endif()
|
||||
|
||||
# Generated LXDR header; ipc-instance.cpp binds it to the RPC core, and
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// config-macos.mm: macOS CFPreferences configuration backend
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include <dawn-config.h>
|
||||
|
||||
#include "libdn.h"
|
||||
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dawn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
void
|
||||
fail(Error *error, const char *message)
|
||||
{
|
||||
if (error)
|
||||
*error = {Error::Code::Io, message};
|
||||
}
|
||||
|
||||
CFStringRef
|
||||
make_cfstring(string_view value, Error *error)
|
||||
{
|
||||
CFStringRef result = CFStringCreateWithBytes(kCFAllocatorDefault,
|
||||
reinterpret_cast<const UInt8 *>(value.data()), CFIndex(value.size()),
|
||||
kCFStringEncodingUTF8, false);
|
||||
if (!result)
|
||||
fail(error, "invalid UTF-8 configuration string");
|
||||
return result;
|
||||
}
|
||||
|
||||
optional<string>
|
||||
to_utf8(CFStringRef value, Error *error)
|
||||
{
|
||||
const CFIndex length = CFStringGetLength(value);
|
||||
const CFIndex maximum =
|
||||
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||
vector<char> bytes(static_cast<size_t>(maximum));
|
||||
if (!CFStringGetCString(
|
||||
value, bytes.data(), maximum, kCFStringEncodingUTF8)) {
|
||||
fail(error, "cannot convert configuration string to UTF-8");
|
||||
return nullopt;
|
||||
}
|
||||
return string(bytes.data());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
optional<string>
|
||||
config_get(string_view key, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
CFStringRef cf_key = make_cfstring(key, error);
|
||||
if (!cf_key)
|
||||
return nullopt;
|
||||
CFPropertyListRef value = CFPreferencesCopyValue(cf_key,
|
||||
CFSTR(DAWN_NAMESPACE), kCFPreferencesCurrentUser,
|
||||
kCFPreferencesAnyHost);
|
||||
CFRelease(cf_key);
|
||||
if (!value)
|
||||
return nullopt;
|
||||
if (CFGetTypeID(value) != CFStringGetTypeID()) {
|
||||
CFRelease(value);
|
||||
fail(error, "configuration value is not a string");
|
||||
return nullopt;
|
||||
}
|
||||
optional<string> result = to_utf8(CFStringRef(value), error);
|
||||
CFRelease(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool
|
||||
config_set(string_view key, string_view value, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
CFStringRef cf_key = make_cfstring(key, error);
|
||||
CFStringRef cf_value = make_cfstring(value, error);
|
||||
if (!cf_key || !cf_value) {
|
||||
if (cf_key)
|
||||
CFRelease(cf_key);
|
||||
if (cf_value)
|
||||
CFRelease(cf_value);
|
||||
return false;
|
||||
}
|
||||
CFPreferencesSetValue(cf_key, cf_value, CFSTR(DAWN_NAMESPACE),
|
||||
kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
|
||||
CFRelease(cf_key);
|
||||
CFRelease(cf_value);
|
||||
if (!CFPreferencesSynchronize(CFSTR(DAWN_NAMESPACE),
|
||||
kCFPreferencesCurrentUser, kCFPreferencesAnyHost)) {
|
||||
fail(error, "cannot synchronize configuration");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dawn
|
||||
@@ -0,0 +1,289 @@
|
||||
//
|
||||
// config-unix.cpp: XDG configuration file backend
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include <dawn-config.h>
|
||||
|
||||
#include "libdn.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
namespace fs = filesystem;
|
||||
|
||||
namespace dawn
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
|
||||
string
|
||||
trim(string_view value)
|
||||
{
|
||||
const size_t first = value.find_first_not_of(" \t");
|
||||
if (first == string_view::npos)
|
||||
return {};
|
||||
const size_t last = value.find_last_not_of(" \t");
|
||||
return string(value.substr(first, last - first + 1));
|
||||
}
|
||||
|
||||
string
|
||||
desktop_unescape(string_view value)
|
||||
{
|
||||
string out;
|
||||
out.reserve(value.size());
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (value[i] != '\\' || i + 1 == value.size()) {
|
||||
out += value[i];
|
||||
continue;
|
||||
}
|
||||
switch (value[++i]) {
|
||||
case 'n': out += '\n'; break;
|
||||
case 'r': out += '\r'; break;
|
||||
case 't': out += '\t'; break;
|
||||
case 's': out += ' '; break;
|
||||
default: out += value[i]; break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
string
|
||||
desktop_escape(string_view value)
|
||||
{
|
||||
string out;
|
||||
out.reserve(value.size());
|
||||
for (const char c : value) {
|
||||
switch (c) {
|
||||
case '\\': out += "\\\\"; break;
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default: out += c; break;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
IniFile
|
||||
ini_parse(string_view text)
|
||||
{
|
||||
IniFile ini;
|
||||
IniGroup *group = nullptr;
|
||||
for (size_t offset = 0; offset <= text.size();) {
|
||||
const size_t end = text.find_first_of("\r\n", offset);
|
||||
string line(text.substr(offset,
|
||||
end == string::npos ? string::npos : end - offset));
|
||||
const string stripped = trim(line);
|
||||
if (stripped.empty() || stripped[0] == '#') {
|
||||
if (!group)
|
||||
ini.preamble.push_back(line);
|
||||
} else if (stripped.size() >= 2 && stripped.front() == '[' &&
|
||||
stripped.back() == ']' && stripped.find('=') == string::npos) {
|
||||
ini.groups.push_back(
|
||||
{stripped.substr(1, stripped.size() - 2), {}});
|
||||
group = &ini.groups.back();
|
||||
} else if (group) {
|
||||
const size_t equals = line.find('=');
|
||||
if (equals != string::npos)
|
||||
group->keys.emplace_back(
|
||||
trim(string_view(line).substr(0, equals)),
|
||||
string(string_view(line).substr(equals + 1)));
|
||||
}
|
||||
if (end == string::npos)
|
||||
break;
|
||||
offset = end +
|
||||
(text[end] == '\r' && end + 1 < text.size() && text[end + 1] == '\n'
|
||||
? 2
|
||||
: 1);
|
||||
}
|
||||
return ini;
|
||||
}
|
||||
|
||||
string
|
||||
ini_serialize(const IniFile &ini)
|
||||
{
|
||||
string out;
|
||||
for (const string &line : ini.preamble)
|
||||
out += line + '\n';
|
||||
for (const IniGroup &group : ini.groups) {
|
||||
out += '[' + group.name + "]\n";
|
||||
for (const auto &[key, value] : group.keys)
|
||||
out += key + '=' + value + '\n';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
string
|
||||
ini_get(const IniGroup &group, string_view key)
|
||||
{
|
||||
for (const auto &[name, value] : group.keys)
|
||||
if (name == key)
|
||||
return value;
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
ini_set(IniGroup &group, string_view key, string_view value)
|
||||
{
|
||||
for (auto &[name, stored] : group.keys)
|
||||
if (name == key) {
|
||||
stored = value;
|
||||
return;
|
||||
}
|
||||
group.keys.emplace_back(key, value);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
using detail::IniFile;
|
||||
using detail::IniGroup;
|
||||
|
||||
void
|
||||
fail(Error *error, string message)
|
||||
{
|
||||
if (error)
|
||||
*error = {Error::Code::Io, std::move(message)};
|
||||
}
|
||||
|
||||
optional<pair<string, string>>
|
||||
split_key(string_view key)
|
||||
{
|
||||
const size_t slash = key.rfind('/');
|
||||
if (slash == string_view::npos || slash == 0 || slash + 1 == key.size())
|
||||
return nullopt;
|
||||
return pair<string, string>{string(key.substr(0, slash)),
|
||||
string(key.substr(slash + 1))};
|
||||
}
|
||||
|
||||
fs::path
|
||||
config_path(Error *error)
|
||||
{
|
||||
if (const char *xdg = getenv("XDG_CONFIG_HOME"); xdg && *xdg) {
|
||||
fs::path base(xdg);
|
||||
if (base.is_absolute())
|
||||
return base / DAWN_NAMESPACE / "dawn.conf";
|
||||
}
|
||||
if (const char *home = getenv("HOME"); home && *home)
|
||||
return fs::path(home) / ".config" / DAWN_NAMESPACE / "dawn.conf";
|
||||
fail(error, "cannot locate the user configuration directory");
|
||||
return {};
|
||||
}
|
||||
|
||||
optional<IniFile>
|
||||
load_ini(const fs::path &path, Error *error)
|
||||
{
|
||||
error_code ec;
|
||||
if (!fs::exists(path, ec)) {
|
||||
if (ec)
|
||||
fail(error, "cannot inspect configuration file: " + ec.message());
|
||||
else
|
||||
return IniFile{};
|
||||
return nullopt;
|
||||
}
|
||||
ifstream input(path, ios::binary);
|
||||
if (!input) {
|
||||
fail(error, "cannot open configuration file");
|
||||
return nullopt;
|
||||
}
|
||||
string text((istreambuf_iterator<char>(input)), istreambuf_iterator<char>());
|
||||
if (input.bad()) {
|
||||
fail(error, "cannot read configuration file");
|
||||
return nullopt;
|
||||
}
|
||||
return detail::ini_parse(text);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
optional<string>
|
||||
config_get(string_view key, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
const auto parts = split_key(key);
|
||||
if (!parts) {
|
||||
fail(error, "invalid configuration key");
|
||||
return nullopt;
|
||||
}
|
||||
const fs::path path = config_path(error);
|
||||
if (path.empty())
|
||||
return nullopt;
|
||||
const optional<IniFile> ini = load_ini(path, error);
|
||||
if (!ini)
|
||||
return nullopt;
|
||||
for (const IniGroup &group : ini->groups) {
|
||||
if (group.name != parts->first)
|
||||
continue;
|
||||
for (const auto &[name, value] : group.keys)
|
||||
if (name == parts->second)
|
||||
return detail::desktop_unescape(value);
|
||||
}
|
||||
return nullopt;
|
||||
}
|
||||
|
||||
bool
|
||||
config_set(string_view key, string_view value, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
const auto parts = split_key(key);
|
||||
if (!parts) {
|
||||
fail(error, "invalid configuration key");
|
||||
return false;
|
||||
}
|
||||
const fs::path path = config_path(error);
|
||||
if (path.empty())
|
||||
return false;
|
||||
optional<IniFile> ini = load_ini(path, error);
|
||||
if (!ini)
|
||||
return false;
|
||||
IniGroup *wanted = nullptr;
|
||||
for (IniGroup &group : ini->groups)
|
||||
if (group.name == parts->first) {
|
||||
wanted = &group;
|
||||
break;
|
||||
}
|
||||
if (!wanted) {
|
||||
ini->groups.push_back({parts->first, {}});
|
||||
wanted = &ini->groups.back();
|
||||
}
|
||||
detail::ini_set(*wanted, parts->second, detail::desktop_escape(value));
|
||||
|
||||
error_code ec;
|
||||
fs::create_directories(path.parent_path(), ec);
|
||||
if (ec) {
|
||||
fail(error, "cannot create configuration directory: " + ec.message());
|
||||
return false;
|
||||
}
|
||||
const fs::path temporary = path.string() + ".new";
|
||||
const string data = detail::ini_serialize(*ini);
|
||||
{
|
||||
ofstream output(temporary, ios::binary | ios::trunc);
|
||||
if (!output || !output.write(data.data(), streamsize(data.size())) ||
|
||||
!output.flush()) {
|
||||
fail(error, "cannot write configuration file");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
fs::rename(temporary, path, ec);
|
||||
if (ec) {
|
||||
fail(error, "cannot replace configuration file: " + ec.message());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dawn
|
||||
@@ -0,0 +1,162 @@
|
||||
//
|
||||
// config-windows.cpp: Windows registry configuration backend
|
||||
//
|
||||
// Copyright The Dawn Authors
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
|
||||
#include <dawn-config.h>
|
||||
|
||||
#include "libdn.h"
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
|
||||
namespace dawn
|
||||
{
|
||||
namespace
|
||||
{
|
||||
|
||||
void
|
||||
fail(Error *error, const char *operation, LSTATUS status = ERROR_SUCCESS)
|
||||
{
|
||||
if (!error)
|
||||
return;
|
||||
error->code = Error::Code::Io;
|
||||
error->message = operation;
|
||||
if (status != ERROR_SUCCESS)
|
||||
error->message += ": Windows error " + to_string(status);
|
||||
}
|
||||
|
||||
optional<wstring>
|
||||
to_wide(string_view value, Error *error)
|
||||
{
|
||||
if (value.empty())
|
||||
return wstring{};
|
||||
const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||
value.data(), int(value.size()), nullptr, 0);
|
||||
if (!size) {
|
||||
fail(error, "invalid UTF-8 configuration string", GetLastError());
|
||||
return nullopt;
|
||||
}
|
||||
wstring out(size_t(size), L'\0');
|
||||
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(),
|
||||
int(value.size()), out.data(), size)) {
|
||||
fail(error, "cannot convert configuration string", GetLastError());
|
||||
return nullopt;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
optional<string>
|
||||
to_utf8(wstring_view value, Error *error)
|
||||
{
|
||||
if (value.empty())
|
||||
return string{};
|
||||
const int size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS,
|
||||
value.data(), int(value.size()), nullptr, 0, nullptr, nullptr);
|
||||
if (!size) {
|
||||
fail(error, "invalid UTF-16 configuration string", GetLastError());
|
||||
return nullopt;
|
||||
}
|
||||
string out(size_t(size), '\0');
|
||||
if (!WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(),
|
||||
int(value.size()), out.data(), size, nullptr, nullptr)) {
|
||||
fail(error, "cannot convert configuration string", GetLastError());
|
||||
return nullopt;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct RegistryName {
|
||||
wstring subkey;
|
||||
wstring value;
|
||||
};
|
||||
|
||||
optional<RegistryName>
|
||||
registry_name(string_view key, Error *error)
|
||||
{
|
||||
const size_t slash = key.rfind('/');
|
||||
if (slash == string_view::npos || slash == 0 || slash + 1 == key.size()) {
|
||||
fail(error, "invalid configuration key");
|
||||
return nullopt;
|
||||
}
|
||||
string subkey = "Software\\" DAWN_NAMESPACE "\\";
|
||||
subkey += key.substr(0, slash);
|
||||
for (char &c : subkey)
|
||||
if (c == '/')
|
||||
c = '\\';
|
||||
auto wide_subkey = to_wide(subkey, error);
|
||||
auto wide_value = to_wide(key.substr(slash + 1), error);
|
||||
if (!wide_subkey || !wide_value)
|
||||
return nullopt;
|
||||
return RegistryName{std::move(*wide_subkey), std::move(*wide_value)};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
optional<string>
|
||||
config_get(string_view key, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
const auto name = registry_name(key, error);
|
||||
if (!name)
|
||||
return nullopt;
|
||||
DWORD bytes = 0;
|
||||
LSTATUS status = RegGetValueW(HKEY_CURRENT_USER, name->subkey.c_str(),
|
||||
name->value.c_str(), RRF_RT_REG_SZ, nullptr, nullptr, &bytes);
|
||||
if (status == ERROR_FILE_NOT_FOUND)
|
||||
return nullopt;
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fail(error, "cannot read configuration value", status);
|
||||
return nullopt;
|
||||
}
|
||||
vector<wchar_t> data((bytes + sizeof(wchar_t) - 1) / sizeof(wchar_t));
|
||||
status = RegGetValueW(HKEY_CURRENT_USER, name->subkey.c_str(),
|
||||
name->value.c_str(), RRF_RT_REG_SZ, nullptr, data.data(), &bytes);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fail(error, "cannot read configuration value", status);
|
||||
return nullopt;
|
||||
}
|
||||
size_t length = bytes / sizeof(wchar_t);
|
||||
while (length && data[length - 1] == L'\0')
|
||||
--length;
|
||||
return to_utf8(wstring_view(data.data(), length), error);
|
||||
}
|
||||
|
||||
bool
|
||||
config_set(string_view key, string_view value, Error *error)
|
||||
{
|
||||
if (error)
|
||||
*error = {};
|
||||
const auto name = registry_name(key, error);
|
||||
const auto data = to_wide(value, error);
|
||||
if (!name || !data)
|
||||
return false;
|
||||
HKEY handle = nullptr;
|
||||
LSTATUS status = RegCreateKeyExW(HKEY_CURRENT_USER, name->subkey.c_str(), 0,
|
||||
nullptr, 0, KEY_SET_VALUE, nullptr, &handle, nullptr);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fail(error, "cannot open configuration key", status);
|
||||
return false;
|
||||
}
|
||||
status = RegSetValueExW(handle, name->value.c_str(), 0, REG_SZ,
|
||||
reinterpret_cast<const BYTE *>(data->c_str()),
|
||||
DWORD((data->size() + 1) * sizeof(wchar_t)));
|
||||
RegCloseKey(handle);
|
||||
if (status != ERROR_SUCCESS) {
|
||||
fail(error, "cannot write configuration value", status);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dawn
|
||||
@@ -10,9 +10,12 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dawn
|
||||
@@ -74,6 +77,34 @@ struct Error {
|
||||
}
|
||||
};
|
||||
|
||||
/// Read and write an opaque application configuration value.
|
||||
/// A missing value is not an error and returns std::nullopt.
|
||||
std::optional<std::string> config_get(std::string_view key, Error *error = nullptr);
|
||||
bool config_set(
|
||||
std::string_view key, std::string_view value, Error *error = nullptr);
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
struct IniGroup {
|
||||
std::string name;
|
||||
std::vector<std::pair<std::string, std::string>> keys;
|
||||
};
|
||||
|
||||
struct IniFile {
|
||||
std::vector<std::string> preamble;
|
||||
std::vector<IniGroup> groups;
|
||||
};
|
||||
|
||||
IniFile ini_parse(std::string_view text);
|
||||
std::string ini_serialize(const IniFile &ini);
|
||||
std::string ini_get(const IniGroup &group, std::string_view key);
|
||||
void ini_set(IniGroup &group, std::string_view key, std::string_view value);
|
||||
std::string desktop_unescape(std::string_view value);
|
||||
std::string desktop_escape(std::string_view value);
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class Cmm;
|
||||
class Profile;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user