aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/configreader.vala
blob: 833663769377b2fbaa273d89a3d63de25d6a7f7b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
// 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;
	}
}