// 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 or duplicate keys joined with ', ' var warning = ngettext("unknown or duplicate key in config: %s", "unknown or duplicate 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 . 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 . 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 . 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; } }