aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/configreader.vala
diff options
context:
space:
mode:
authorWynn Wolf Arbor2019-10-30 19:11:16 +0100
committerWynn Wolf Arbor2020-02-04 14:33:13 +0100
commit00702a3b764139a28b4de89f48ec1e38f1c58150 (patch)
tree8ab9563b3d462a60335ec489d2371df474201d0d /configreader.vala
parentf72f05cd8e157e5f433dca21de2a8d7fb2411436 (diff)
downloadweltschmerz-00702a3b764139a28b4de89f48ec1e38f1c58150.tar.gz
Improve configuration handling
This commit improves and simplifies weltschmerz's configuration handling. Obtaining and parsing the KeyFile is split out into ConfigReader, which is fully agnostic of the specific configuration values. Config now contains all configuration values in the form of primitive types or class instances, and no longer delegates access to the KeyFile structure directly. This means that the configuration file is read once, and then kept in the Config instance. Indirectly this commit also fixes a bug where weltschmerz would segfault if one of the palette entries contained an invalid value.
Diffstat (limited to 'configreader.vala')
-rw-r--r--configreader.vala188
1 files changed, 188 insertions, 0 deletions
diff --git a/configreader.vala b/configreader.vala
new file mode 100644
index 0000000..a57a7ab
--- /dev/null
+++ b/configreader.vala
@@ -0,0 +1,188 @@
+// 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) {
+ string k = keys.length > 1 ? "keys" : "key";
+ append_warning("Unknown %s in config: %s".printf(k, string.joinv(", ", keys)));
+ }
+ }
+
+ 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)) {
+ 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:
+ append_warning("invalid cursor shape '%s' in %s.%s".printf(cursor, group, key));
+ return BLOCK;
+ }
+ }
+}