Configuration

This commit is contained in:
2026-08-30 06:30:05 +02:00
parent 19c5d3a050
commit ec132e7368
14 changed files with 852 additions and 185 deletions
+1
View File
@@ -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)
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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)
+9 -2
View File
@@ -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
+111
View File
@@ -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
+289
View File
@@ -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
+162
View File
@@ -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
+31
View File
@@ -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;