aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/configreader.vala
blob: a57a7ab9c4bb70d26267d3577f31e97936e9e01b (plain) (tree)



























































































































































































                                                                                                                
// 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;
		}
	}
}