// What follows is arguably the least readily understandable part of
// weltschmerz; some pointers and design notes shall follow.
//
// The purpose of this class is to read the key-value configuration file from
// disk and turn it into an in-memory representation of GLib's KeyFile
// structure. We do not stray from this format, yet the error handling and some
// design implications make it a bit harder than *just* parsing a file.
//
// In particular, a core design decision is to provide reasonable defaults to
// the user and to, wherever possible, fall back to a configuration that is at
// least useable. Short of a bug in GLib's KeyFile implementation, this code
// *should* gracefully deal with any sort of input: a missing configuration
// file, a corrupted or wrongly encoded configuration file, etc.
//
// Another decision is to report any noteworthy error to the user, and to make
// sure that issues with the configuration file are reported as accurately as
// possible. This includes particularly the reporting of entries that were
// *not* accessed by the program at all, which can indicate a small oversight
// like a typo, or a deprecated config entry.
//
// Most of the information in this file based on the official documentation,
// but a few parts (the ones there is no documentation for) are based on my
// reading of the GLib C code and live testing. Therefore, I cannot guarantee
// complete accuracy.
//
// Notable GLib resources:
// https://valadoc.org/glib-2.0/GLib.KeyFile.html
// https://valadoc.org/glib-2.0/GLib.KeyFileError.html
class ConfigReader {
KeyFile keyfile = new KeyFile();
string[] warnings;
public ConfigReader (string path) {
try {
keyfile.load_from_file(path, NONE);
} catch (Error e) {
// The GLib documentation does not make this clear, but the only
// KeyFileErrors that will be reported when loading the file from
// disk are KeyFileError.PARSE and KeyFileError.UNKNOWN_ENCODING
//
// KeyFileError.NOT_FOUND will never be returned, FileError.NOENT
// takes its place instead. Since we can gracefully fall back to
// default values, we specifically ignore this error.
if (e is FileError.NOENT)
return;
// We want to warn the user about any other error, and set keyfile
// to null. At this point the file has already been fully parsed
// by GLib, but we do not trust it to have read anything correctly
// at this point, so we destroy the in-memory representation.
append_warning(e.message);
keyfile = null;
}
}
public string[] get_warnings() {
return warnings;
}
void append_warning(string message) {
warning(message);
warnings += "• " + Markup.escape_text(message);
}
// This method is called from all three read_* methods if they encountered
// any KeyFileErrors whilst accessing keys in keyfile. At this point, the
// following KeyFileErrors are possible: INVALID_VALUE, UNKNOWN_ENCODING,
// KEY_NOT_FOUND, and GROUP_NOT_FOUND.
//
// The latter two are insignificant since we can fall back to default
// values.
//
// INVALID_VALUE is treated specially here to remove a potentially
// confusing warning message about unknown keys (see log_unknown_keys).
//
// Finally, UNKNOWN_ENCODING is treated normally.
void handle_error(KeyFileError e, string group, string key) {
if (e is KeyFileError.INVALID_VALUE) {
try {
// Remove a known key with an invalid value
keyfile.remove_key(group, key);
} catch (KeyFileError e) {
debug("Could not remove existing and valid key entry");
}
} else if (e is KeyFileError.UNKNOWN_ENCODING) {
append_warning(e.message);
}
}
// In order to keep track of entries that were not accessed by the program
// at all, we remove all successfully read entries from the KeyFile. Any
// entries that are left over have not been used and can then be reported
// to the user.
public void log_unknown_keys() {
if (keyfile == null)
return;
string[] keys = {};
try {
foreach(var group in keyfile.get_groups()) {
foreach(var key in keyfile.get_keys(group)) {
keys += string.join(".", group, key);
}
}
} catch (KeyFileError e) {}
if (keys.length > 0) {
var keylist = string.joinv(", ", keys);
// TRANSLATORS: %s is the list of unknown keys joined with ', '
var warning = ngettext("unknown key in config: %s",
"unknown keys in config: %s", keys.length).printf(keylist);
append_warning(warning);
}
}
public string? read_string(string group, string key, string? default) {
if (keyfile == null)
return default;
try {
string str = keyfile.get_string(group, key);
keyfile.remove_key(group, key);
return str;
} catch (KeyFileError e) {
handle_error(e, group, key);
return default;
}
}
public int? read_integer(string group, string key, int? default) {
if (keyfile == null)
return default;
try {
int integer = keyfile.get_integer(group, key);
keyfile.remove_key(group, key);
return integer;
} catch (KeyFileError e) {
handle_error(e, group, key);
return default;
}
}
public bool? read_boolean(string group, string key, bool? default) {
if (keyfile == null)
return default;
try {
bool boolean = keyfile.get_boolean(group, key);
keyfile.remove_key(group, key);
return boolean;
} catch (KeyFileError e) {
handle_error(e, group, key);
return default;
}
}
public Gdk.RGBA? read_colour(string group, string key, string? default) {
string str = read_string(group, key, default);
if (str == null)
return null;
var rgba = Gdk.RGBA();
if (!rgba.parse(str)) {
// TRANSLATORS: First %s is parsed colour, %s.%s is <group>.<key> from the config file
append_warning(_("invalid colour '%s' in %s.%s").printf(str, group, key));
rgba.parse(default);
return rgba;
}
return rgba;
}
public Vte.CursorShape read_cursor(string group, string key, string default) {
string cursor = read_string(group, key, default);
switch (cursor) {
case "block":
return BLOCK;
case "beam":
return IBEAM;
case "underline":
return UNDERLINE;
default:
// TRANSLATORS: First %s is parsed shape, %s.%s is <group>.<key> from the config file
append_warning(_("invalid cursor shape '%s' in %s.%s").printf(cursor, group, key));
return BLOCK;
}
}
public Vte.CursorBlinkMode read_blink(string group, string key, string default) {
string blink = read_string(group, key, default);
switch(blink) {
case "true":
return ON;
case "false":
return OFF;
case "system":
return SYSTEM;
default:
// TRANSLATORS: First %s is parsed blink setting, %s.%s is <group>.<key> from the config file
append_warning(_("invalid cursor blink setting '%s' in %s.%s").printf(blink, group, key));
return SYSTEM;
}
}
public OpenWithProgram[] read_open_with(OpenWithProgram[] default) {
if (keyfile == null)
return default;
OpenWithProgram[] programs = {};
string[] keys;
try {
keys = keyfile.get_keys("open-with");
} catch (GLib.KeyFileError e) {
return default;
}
foreach (var name in keys) {
var command_string = read_string("open-with", name, null);
if (command_string == null)
continue;
string[] command;
try {
Shell.parse_argv(command_string, out command);
} catch (ShellError e) {
append_warning(_("ignoring open-with.%s due to malformed command: %s").printf(name, e.message));
continue;
}
if (!("%" in command)) {
append_warning(_("ignoring command in open-with.%s: missing '%%'").printf(name));
continue;
}
OpenWithProgram program = {name, command};
programs += program;
}
return programs;
}
}