[Controller] Add initial-windows property
[tilda-gobject.git] / tilda-config.c
index c8afbc2..6563312 100644 (file)
+#include "tilda-config.h"
+
+#include "tilda.h"
+#include "tilda-types.h"
 #include "debug.h"
 #include "translation.h"
-#include "tilda-config.h"
 
-GHashTable *config_defaults = NULL;
-GKeyFile   *config_userprefs = NULL;
+#include <glib-object.h> /* for GType */
+#include <stdlib.h> /* for exit() */
+
+GKeyFile *config_defaults = NULL;
+GKeyFile *config_userprefs = NULL;
 
 /*
  * TODO:
- *
- * Add some capability to version the config file. Maybe a [meta] section.
- *
- * Make it possible to get the version number without firing up the config system
- * as a whole. Or maybe just add the migration here, in this file, upon startup.
- *
- * The whole idea of this is that each of the objects that accesses the config
- * system implements their own lookup engine.
+ * 1) Configuration forward-migration, automatically, on startup.
  */
 
-/*
- * The main idea behind this configuration system is that each of the objects
- * that accesses the config system implements their own lookup scheme. Since
- * both TildaWindow and TildaTerminal need to lookup things different ways,
- * I thought this would be the most logical solution.
+/* The main idea behind this configuration system is that each type of GObject
+ * that needs to use the system implements its own lookup scheme. I tried to
+ * do this as generically as possible, by using just the GObject property names,
+ * and then looking up their type and using that to use the correct parsing
+ * function.
+ *
+ * It is basically function overloading (for example, from C++) but done in C.
+ * Rather complicated, if you ask me. Also a complete pain. But the result
+ * sure is nice.
  */
 
-/**
- * This function adds all of the defaults to the defaults hashtable.
- * It just uses their names, since there are currently no conflicts
- * in the properties of TildaWindow and TildaTerminal. If conflicts
- * become a problem in the future, prefix them with "w-" and "t-",
- * for example. Then fix up all callers. :) */
-static void
-tilda_config_setup_defaults ()
+static gchar *
+tilda_config_lookup_version (GKeyFile *keyfile, GError **error)
+{
+       debug_enter  ();
+
+       return g_key_file_get_string (keyfile, "Meta", "version", error);
+}
+
+static gboolean
+tilda_config_parse_integer (GKeyFile    *keyfile,
+                                                       const gchar *group_name,
+                                                       const gchar *key,
+                                                       GValue      *value,
+                                                       GError     **user_error)
+{
+       debug_enter  ();
+       debug_assert (G_IS_VALUE(value));
+
+       GError *error = NULL;
+       gint ret;
+
+       /* Try to get the value */
+       ret = g_key_file_get_integer (keyfile, group_name, key, &error);
+
+       /* Check for error */
+       if (error)
+       {
+               g_propagate_error (user_error, error);
+               return FALSE;
+       }
+
+       /* We successfully parsed the int, so set it in the GValue */
+       g_value_set_int (value, ret);
+
+       return TRUE;
+}
+
+static gboolean
+tilda_config_parse_boolean (GKeyFile    *keyfile,
+                                                       const gchar *group_name,
+                                                       const gchar *key,
+                                                       GValue      *value,
+                                                       GError     **user_error)
+{
+       debug_enter  ();
+       debug_assert (G_IS_VALUE(value));
+
+       GError *error = NULL;
+       gboolean ret;
+
+       /* Try to get the value */
+       ret = g_key_file_get_boolean (keyfile, group_name, key, &error);
+
+       /* Check for error */
+       if (error)
+       {
+               g_propagate_error (user_error, error);
+               return FALSE;
+       }
+
+       /* Success */
+       g_value_set_boolean (value, ret);
+
+       return TRUE;
+}
+
+static gboolean
+tilda_config_parse_string (GKeyFile    *keyfile,
+                                                  const gchar *group_name,
+                                                  const gchar *key,
+                                                  GValue      *value,
+                                                  GError **user_error)
+{
+       debug_enter  ();
+       debug_assert (G_IS_VALUE(value));
+
+       GError *error = NULL;
+       gchar *ret;
+
+       /* Try to get the value */
+       ret = g_key_file_get_string (keyfile, group_name, key, &error);
+
+       /* Check for error */
+       if (error)
+       {
+               g_propagate_error (user_error, error);
+               return FALSE;
+       }
+
+       /* Success */
+       g_value_set_string (value, ret);
+
+       return TRUE;
+}
+
+#define key_is(STR) (g_ascii_strcasecmp(key,(STR)) == 0)
+
+static gboolean
+tilda_config_parse_enum (GKeyFile    *keyfile,
+                                                const gchar *group_name,
+                                                const gchar *key,
+                                                GValue      *value,
+                                                GError     **user_error)
+{
+       gchar *ret;
+       GError *error = NULL;
+       GEnumClass *enum_class;
+       GEnumValue *enum_value;
+
+       if (key_is("backspace-binding") || key_is("delete-binding"))
+               enum_class = g_type_class_peek (vte_terminal_erase_binding_get_type());
+       else if (key_is("dynamic-title"))
+               enum_class = g_type_class_peek (tilda_dynamic_title_get_type());
+       else if (key_is("exit-action"))
+               enum_class = g_type_class_peek (tilda_child_exit_action_get_type());
+       else if (key_is("scrollbar-position"))
+               enum_class = g_type_class_peek (tilda_scrollbar_position_get_type());
+       else if (key_is("animation-orientation") || key_is("tab-position"))
+               enum_class = g_type_class_peek (gtk_position_type_get_type());
+       else
+       {
+               g_critical ("FIXME: developer error -- unknown enum key used: %s\n", key);
+               exit (1);
+               return FALSE;
+       }
+
+       /* Get the value from the config as a string */
+       ret = g_key_file_get_string (keyfile, group_name, key, &error);
+
+       if (error)
+       {
+               g_propagate_error (user_error, error);
+               return FALSE;
+       }
+
+       enum_value = g_enum_get_value_by_nick (enum_class, ret);
+
+       if (!enum_value)
+       {
+               /* This is exactly the same error that is returned by g_key_file_get_integer()
+                * when it cannot correctly parse the value. */
+               g_set_error (user_error,
+                                        G_KEY_FILE_ERROR,
+                                        G_KEY_FILE_ERROR_INVALID_VALUE,
+                                        _("Key file contains key '%s' in group '%s' which has value that cannot be interpreted."), key, group_name);
+               return FALSE;
+       }
+
+       /* All was successful, let's set it */
+       g_value_set_enum (value, enum_value->value);
+       return TRUE;
+}
+
+gboolean
+tilda_controller_set_property_from_config (TildaController *self, const gchar *property)
+{
+       debug_enter  ();
+       debug_assert (TILDA_IS_CONTROLLER(self));
+
+       GError *error = NULL;
+       gboolean ret;
+
+       GParamSpec *pspec;
+       GValue *value = g_malloc0(sizeof(GValue));
+       gboolean (*parse_func) (GKeyFile *keyfile, const gchar *group_name, const gchar *key, GValue *value, GError **error);
+
+       /* Get the pspec for this property */
+       pspec = g_object_class_find_property (G_OBJECT_GET_CLASS(self), property);
+
+       /* Make sure that this property exists */
+       if (pspec == NULL)
+       {
+               g_critical ("FIXME: developer error -- unable to find property: %s\n", property);
+               exit (1);
+       }
+
+       /* Initialize the GValue that is going to hold the returned value */
+       g_value_init (value, pspec->value_type);
+
+       /* Set the correct function to do the parsing */
+       if      (g_type_is_a (pspec->value_type, G_TYPE_INT))
+               parse_func = tilda_config_parse_integer;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_BOOLEAN))
+               parse_func = tilda_config_parse_boolean;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_STRING))
+               parse_func = tilda_config_parse_string;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_ENUM))
+               parse_func = tilda_config_parse_enum;
+       else
+       {
+               g_critical ("FIXME: developer error -- unknown property type: %s\n", g_type_name(pspec->value_type));
+               exit(1);
+       }
+
+       /* Do the [Controller] lookup */
+       ret = parse_func (config_userprefs, "Controller", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [Global] lookup */
+       ret = parse_func (config_userprefs, "Global", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [controller-defaults] lookup */
+       ret = parse_func (config_defaults, "controller-defaults", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+
+               /* This is somewhat of a nasty hack, but I really do want to just set the property to
+                * NULL if there is no default, but ONLY for strings. */
+               if (parse_func == tilda_config_parse_string)
+               {
+                       g_value_set_string (value, NULL);
+                       goto success;
+               }
+       }
+       else
+               goto success;
+
+//failure:
+       g_critical (_("Unable to find a value for controller property: %s\n"), property);
+       return FALSE;
+
+success:
+       g_object_set_property (G_OBJECT(self), property, value);
+       g_value_unset (value);
+       g_free (value);
+       return TRUE;
+}
+
+gboolean
+tilda_window_set_property_from_config (TildaWindow *self, const gchar *property)
 {
-       /* NOTE: All properties must be specified as strings. This sucks, but it really
-        * is easiest to keep it this way. We can then implement custom functions to
-        * change the values as we need them. */
-
-       /* Window properties */
-       g_hash_table_insert (config_defaults, "key", NULL);
-       g_hash_table_insert (config_defaults, "height", "300");
-       g_hash_table_insert (config_defaults, "width", "600");
-       g_hash_table_insert (config_defaults, "x-position", "0");
-       g_hash_table_insert (config_defaults, "y-position", "0");
-       g_hash_table_insert (config_defaults, "initial-terminals", "1");
-       g_hash_table_insert (config_defaults, "tab-position", "top");
-       g_hash_table_insert (config_defaults, "animation-orientation", "top");
-       g_hash_table_insert (config_defaults, "animation-delay", "15000");
-       g_hash_table_insert (config_defaults, "keep-above", "true");
-       g_hash_table_insert (config_defaults, "skip-taskbar-hint", "true");
-       g_hash_table_insert (config_defaults, "stick", "true");
-       g_hash_table_insert (config_defaults, "hidden-at-start", "false");
-       g_hash_table_insert (config_defaults, "centered-horizontally", "false");
-       g_hash_table_insert (config_defaults, "centered-vertically", "false");
-
-       /* Terminal Properties */
-       g_hash_table_insert (config_defaults, "background-image", NULL);
-       g_hash_table_insert (config_defaults, "shell", NULL);
-       g_hash_table_insert (config_defaults, "font", "Bitstream Vera Sans Mono 12");
-       g_hash_table_insert (config_defaults, "title", "Tilda");
-       g_hash_table_insert (config_defaults, "working-directory", NULL);
-       g_hash_table_insert (config_defaults, "web-browser", "firefox");
-       g_hash_table_insert (config_defaults, "scrollback-lines", "1000");
-       g_hash_table_insert (config_defaults, "transparency-percent", "0");
-       g_hash_table_insert (config_defaults, "backspace-binding", "vte-erase-auto");
-       g_hash_table_insert (config_defaults, "delete-binding", "vte-erase-auto");
-       g_hash_table_insert (config_defaults, "dynamic-title", "after-initial");
-       g_hash_table_insert (config_defaults, "exit-action", "exit");
-       g_hash_table_insert (config_defaults, "scrollbar-position", "right");
-#if 0
-       /* These don't have properties yet! */
-       g_hash_table_insert (config_defaults, "background-color", "0;0;0");
-       g_hash_table_insert (config_defaults, "foreground-color", "255;255;255");
-#endif
-       g_hash_table_insert (config_defaults, "scroll-background", "false");
-       g_hash_table_insert (config_defaults, "scroll-on-output", "false");
-       g_hash_table_insert (config_defaults, "scroll-on-keystroke", "true");
-       g_hash_table_insert (config_defaults, "antialiased", "true");
-       g_hash_table_insert (config_defaults, "allow-bold-text", "true");
-       g_hash_table_insert (config_defaults, "cursor-blinks", "true");
-       g_hash_table_insert (config_defaults, "audible-bell", "false");
-       g_hash_table_insert (config_defaults, "visible-bell", "true");
-       g_hash_table_insert (config_defaults, "double-buffered", "true");
-       g_hash_table_insert (config_defaults, "mouse-autohide", "true");
+       debug_enter  ();
+       debug_assert (TILDA_IS_WINDOW(self));
+
+       gchar *group_name;
+       GError *error = NULL;
+       gboolean ret;
+
+       GParamSpec *pspec;
+       GValue *value = g_malloc0(sizeof(GValue));
+       gboolean (*parse_func) (GKeyFile *keyfile, const gchar *group_name, const gchar *key, GValue *value, GError **error);
+
+       /* Get the pspec for this property */
+       pspec = g_object_class_find_property (G_OBJECT_GET_CLASS(self), property);
+
+       /* Make sure that this property exists */
+       if (pspec == NULL)
+       {
+               g_critical ("FIXME: developer error -- unable to find property: %s\n", property);
+               exit (1);
+       }
+
+       /* Initialize the GValue that is going to hold the returned value */
+       g_value_init (value, pspec->value_type);
+
+       /* Set the correct function to do the parsing */
+       if      (g_type_is_a (pspec->value_type, G_TYPE_INT))
+               parse_func = tilda_config_parse_integer;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_BOOLEAN))
+               parse_func = tilda_config_parse_boolean;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_STRING))
+               parse_func = tilda_config_parse_string;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_ENUM))
+               parse_func = tilda_config_parse_enum;
+       else
+       {
+               g_critical ("FIXME: developer error -- unknown property type: %s\n", g_type_name(pspec->value_type));
+               exit(1);
+       }
+
+       /* Do the [Window] lookup */
+       group_name = g_strdup_printf ("Window%d", self->number);
+       ret = parse_func (config_userprefs, group_name, property, value, &error);
+       g_free (group_name);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [Global] lookup */
+       ret = parse_func (config_userprefs, "Global", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [window-defaults] lookup */
+       ret = parse_func (config_defaults, "window-defaults", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+
+               /* This is somewhat of a nasty hack, but I really do want to just set the property to
+                * NULL if there is no default, but ONLY for strings. */
+               if (parse_func == tilda_config_parse_string)
+               {
+                       g_value_set_string (value, NULL);
+                       goto success;
+               }
+       }
+       else
+               goto success;
+
+//failure:
+       g_critical (_("Unable to find a value for window property: %s\n"), property);
+       return FALSE;
+
+success:
+       g_object_set_property (G_OBJECT(self), property, value);
+       g_value_unset (value);
+       g_free (value);
+       return TRUE;
 }
 
 gboolean
-tilda_config_init (const gchar *filename)
+tilda_terminal_set_property_from_config (TildaTerminal *self, const gchar *property)
 {
+       debug_enter  ();
+       debug_assert (TILDA_IS_TERMINAL(self));
+
+       TildaWindow *parent_window = TILDA_WINDOW(self->parent_window);
+       gchar *group_name;
        GError *error = NULL;
+       gboolean ret;
+
+       GParamSpec *pspec;
+       GValue *value = g_malloc0(sizeof(GValue));
+       gboolean (*parse_func) (GKeyFile *keyfile, const gchar *group_name, const gchar *key, GValue *value, GError **error);
 
-       /* Create the hashtable */
-       config_defaults = g_hash_table_new (g_str_hash, g_str_equal);
+       /* Get the pspec for this property */
+       pspec = g_object_class_find_property (G_OBJECT_GET_CLASS(self), property);
 
-       /* Fill it with the default values */
-       tilda_config_setup_defaults ();
+       /* Make sure that this property exists */
+       if (pspec == NULL)
+       {
+               g_critical ("FIXME: developer error -- unable to find property: %s\n", property);
+               exit (1);
+       }
+
+       /* Initialize the GValue that is going to hold the returned value */
+       g_value_init (value, pspec->value_type);
 
-       /* If we have no file to read from, then we are just using the defaults,
-        * and are done. */
-       if (filename == NULL)
+       /* Set the correct function to do the parsing */
+       if      (g_type_is_a (pspec->value_type, G_TYPE_INT))
+               parse_func = tilda_config_parse_integer;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_BOOLEAN))
+               parse_func = tilda_config_parse_boolean;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_STRING))
+               parse_func = tilda_config_parse_string;
+       else if (g_type_is_a (pspec->value_type, G_TYPE_ENUM))
+               parse_func = tilda_config_parse_enum;
+       else
        {
-               config_userprefs = NULL;
-               return TRUE;
+               g_critical ("FIXME: developer error -- unknown property type: %s\n", g_type_name(pspec->value_type));
+               exit(1);
        }
 
-       /* Create the key file */
+       /* Do the [Window/Terminal] lookup */
+       group_name = g_strdup_printf ("Window%d/Terminal%d", parent_window->number, self->number);
+       ret = parse_func (config_userprefs, group_name, property, value, &error);
+       g_free (group_name);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [Window] lookup */
+       group_name = g_strdup_printf ("Window%d", parent_window->number);
+       ret = parse_func (config_userprefs, group_name, property, value, &error);
+       g_free (group_name);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [Global] lookup */
+       ret = parse_func (config_userprefs, "Global", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+       }
+       else
+               goto success;
+
+       /* Do the [terminal-defaults] lookup */
+       ret = parse_func (config_defaults, "terminal-defaults", property, value, &error);
+
+       if (error)
+       {
+               if (error->code == G_KEY_FILE_ERROR_INVALID_VALUE)
+                       g_warning (error->message);
+
+               g_clear_error (&error);
+
+               /* This is somewhat of a nasty hack, but I really do want to just set the property to
+                * NULL if there is no default, but ONLY for strings. */
+               if (parse_func == tilda_config_parse_string)
+               {
+                       g_value_set_string (value, NULL);
+                       goto success;
+               }
+       }
+       else
+               goto success;
+
+//failure:
+       g_critical (_("Unable to find a value for terminal property: %s\n"), property);
+       return FALSE;
+
+success:
+       g_object_set_property (G_OBJECT(self), property, value);
+       g_value_unset (value);
+       g_free (value);
+       return TRUE;
+}
+
+gboolean
+tilda_config_init (const gchar *userprefs_filename)
+{
+       gchar *defaults_filename;
+       GError *error = NULL;
+
+       /* Call g_type_class_ref() on all enum types that are being used */
+       g_type_class_ref (vte_terminal_erase_binding_get_type());
+       g_type_class_ref (tilda_dynamic_title_get_type());
+       g_type_class_ref (tilda_child_exit_action_get_type());
+       g_type_class_ref (tilda_scrollbar_position_get_type());
+       g_type_class_ref (gtk_position_type_get_type());
+
+       /* Create the defaults file's path */
+       defaults_filename = g_build_filename ("share-tilda.conf", NULL); // FIXME: use /usr/share/tilda
+
+       /* Create the keyfiles */
+       config_defaults = g_key_file_new ();
        config_userprefs = g_key_file_new ();
 
-       if (!g_file_test (filename, G_FILE_TEST_EXISTS))
+       /* Check if the defaults exist, and load them */
+       if (!g_file_test (defaults_filename, G_FILE_TEST_EXISTS))
        {
-               g_warning (_("No config file found, using defaults\n"));
-               return TRUE;
+               g_critical (_("No configuration defaults file found. Tilda may not work.\n"));
+       }
+       else
+       {
+               if (!g_key_file_load_from_file (config_defaults, defaults_filename, G_KEY_FILE_NONE, &error))
+               {
+                       g_critical (_("Error reading configuration defaults: %s\n"), error->message);
+                       g_clear_error (&error);
+               }
        }
 
-       if (!g_key_file_load_from_file (config_userprefs, filename, G_KEY_FILE_NONE, &error))
+       /* Check if the user's config exists, and load it */
+       if (!g_file_test (userprefs_filename, G_FILE_TEST_EXISTS))
        {
-               g_warning (_("Error reading from config file: %s\n"), error->message);
-               g_error_free (error);
-               return FALSE;
+               g_warning (_("No user configuration file found, using defaults\n"));
+       }
+       else
+       {
+               if (!g_key_file_load_from_file (config_userprefs, userprefs_filename, G_KEY_FILE_NONE, &error))
+               {
+                       g_warning (_("Error reading user configuration: %s\n"), error->message);
+                       g_clear_error (&error);
+               }
        }
 
-       /* Everything went ok */
+       /* This is just here for the future. Currently, it is acceptable to run without
+        * a configuration, though Tilda may not work very well. */
        return TRUE;
 }
 
@@ -132,7 +560,9 @@ tilda_config_init (const gchar *filename)
 gboolean
 tilda_config_free ()
 {
-       g_hash_table_destroy (config_defaults);
+       /* Since we never write the config file from within Tilda,
+        * we can just close the files. */
+       g_key_file_free (config_defaults);
        g_key_file_free (config_userprefs);
 
        config_defaults = NULL;