aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/configreader.vala
blob: 4c68901e58e176ef23ce91a00fe277229454d477 (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) {
			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 <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;
	}
}