diff --git a/.dir-locals.el b/.dir-locals.el index 390f382..476ef0e 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -11,5 +11,9 @@ (css-mode . ( (css-indent-offset . 2) )) + (js-mode . ( + (indent-tabs-mode . nil) + (js-indent-level . 2) + )) ) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9d41c06..6a71037 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -12,7 +12,7 @@ stages: - export DEBIAN_FRONTEND=noninteractive - apt-get -y update - apt-get -y build-dep . - - apt-get -y install gcovr + - apt-get -y install gcovr git .image: &image image: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fae72b7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "subprojects/gmobile"] + path = subprojects/gmobile + url = https://gitlab.gnome.org/guidog/gmobile.git diff --git a/README.md b/README.md index b36c67f..db28c03 100644 --- a/README.md +++ b/README.md @@ -79,17 +79,45 @@ app that triggered it crashes. ## Feedback theme Events are then mapped to a specific type of feedback (sound, led, vibra) via a -device specific theme (since devices have different capabilities). +device specific theme - since devices have different capabilities and +different users different needs. Feedbackd is shipped with a default theme `default.json`. -You can add your own themes in one of two ways: +You can add your own themes in multiple ways: 1. By exporting an environment variable `FEEDBACK_THEME` with a path to a valid theme file (not recommended, use for testing only), or 2. By creating a theme file under `$XDG_CONFIG_HOME/feedbackd/themes/default.json`. If `XDG_CONFIG_HOME` environment variable is not set or empty, it will default to `$HOME/.config`, or -3. By adding your theme file to one of the folders in the `XDG_DATA_DIRS` +3. By creating a theme file under `$XDG_CONFIG_HOME/feedbackd/themes/custom.json`. + You only specify the values you want to change in that theme and add an entry + ```json + { + "name: "custom" + "parent-theme": "default" + "profiles" : [ + ...(entries you want to change go here)... + ] + } + ``` + next to the `name` entry in. This has the upside that your theme + gets way smaller and that new entries added to the default theme + will automatically be used by your theme too. See + [here](./tests/data/user-config/feedbackd/themes/custom.json) for + an example. Once you have the file in place, tell feedbackd the + them you want to use: + + gsettings set org.sigxcpu.feedbackd theme custom + + When you want to go back to the default theme just do: + + gsettings reset org.sigxcpu.feedbackd theme + + Note that you can name your theme as you wish but avoid theme names + starting with `__` or `$` as this namespace is reserved. + +4. By adding your theme file to one of the folders in the `XDG_DATA_DIRS` environment variable, appended with `feedbackd/themes/`. This folder isn't created automatically, so you have to create it yourself. Here's an example: ```bash diff --git a/data/org.sigxcpu.feedbackd.gschema.xml b/data/org.sigxcpu.feedbackd.gschema.xml index eba31ba..723f8c1 100644 --- a/data/org.sigxcpu.feedbackd.gschema.xml +++ b/data/org.sigxcpu.feedbackd.gschema.xml @@ -14,6 +14,15 @@ level given on events to the user. + + + 'default' + The feedback theme to use + + The feedback theme determines which feedbacks are triggered when + feedbackd receives an event. + + diff --git a/meson.build b/meson.build index 7835999..3b63564 100644 --- a/meson.build +++ b/meson.build @@ -14,6 +14,13 @@ gio_unix = dependency('gio-unix-2.0', version: '>=2.50.0') glib = dependency('glib-2.0', version: '>=2.50.0') if get_option('daemon') gobject = dependency('gobject-2.0', version: '>=2.50.0') + gmobile_subp = subproject('gmobile', + default_options: [ + 'examples=false', + 'gtk_doc=false', + 'tests=false', + ]) + gmobile = gmobile_subp.get_variable('libgmobile_dep') gsound = dependency('gsound') gudev = dependency('gudev-1.0', version: '>=232') json_glib = dependency('json-glib-1.0') diff --git a/src/fbd-error.c b/src/fbd-error.c new file mode 100644 index 0000000..22993fb --- /dev/null +++ b/src/fbd-error.c @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 Guido Günther + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Guido Günther + */ + +#include "fbd.h" + +GQuark +fbd_error_quark (void) +{ + static GQuark quark = 0; + + if (!quark) + quark = g_quark_from_static_string("fbd"); + + return quark; +} diff --git a/src/fbd-feedback-dummy.c b/src/fbd-feedback-dummy.c index 7d405e7..f742898 100644 --- a/src/fbd-feedback-dummy.c +++ b/src/fbd-feedback-dummy.c @@ -126,3 +126,11 @@ static void fbd_feedback_dummy_init (FbdFeedbackDummy *self) { } + +guint +fbd_feedback_dummy_get_duration (FbdFeedbackDummy *self) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_DUMMY (self), 0); + + return self->duration; +} diff --git a/src/fbd-feedback-dummy.h b/src/fbd-feedback-dummy.h index 2df8328..7c39d1d 100644 --- a/src/fbd-feedback-dummy.h +++ b/src/fbd-feedback-dummy.h @@ -13,4 +13,6 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (FbdFeedbackDummy, fbd_feedback_dummy, FBD, FEEDBACK_DUMMY, FbdFeedbackBase); +guint fbd_feedback_dummy_get_duration (FbdFeedbackDummy *self); + G_END_DECLS diff --git a/src/fbd-feedback-manager.c b/src/fbd-feedback-manager.c index e37cb8d..e0788cf 100644 --- a/src/fbd-feedback-manager.c +++ b/src/fbd-feedback-manager.c @@ -14,6 +14,10 @@ #include "fbd-feedback-vibra.h" #include "fbd-feedback-manager.h" #include "fbd-feedback-theme.h" +#include "fbd-theme-expander.h" + +#define GMOBILE_USE_UNSTABLE_API +#include #include #include @@ -21,14 +25,12 @@ #define FEEDBACKD_SCHEMA_ID "org.sigxcpu.feedbackd" #define FEEDBACKD_KEY_PROFILE "profile" -#define FEEDBACKD_THEME_VAR "FEEDBACK_THEME" +#define FEEDBACKD_KEY_THEME "theme" #define APP_SCHEMA FEEDBACKD_SCHEMA_ID ".application" #define APP_PREFIX "/org/sigxcpu/feedbackd/application/" -#define DEVICE_TREE_PATH "/sys/firmware/devicetree/base/compatible" -#define DEVICE_NAME_MAX 1024 - +#define FEEDBACKD_THEME_VAR "FEEDBACK_THEME" /** * SECTION:fbd-feedback-manager @@ -212,10 +214,15 @@ on_feedbackd_setting_changed (FbdFeedbackManager *self, g_return_if_fail (FBD_IS_FEEDBACK_MANAGER (self)); g_return_if_fail (G_IS_SETTINGS (settings)); - g_return_if_fail (!g_strcmp0 (key, FEEDBACKD_KEY_PROFILE)); - profile = g_settings_get_string (settings, key); - fbd_feedback_manager_set_profile (self, profile); + if (g_str_equal (key, FEEDBACKD_KEY_PROFILE)) { + profile = g_settings_get_string (settings, key); + fbd_feedback_manager_set_profile (self, profile); + } else if (g_str_equal (key, FEEDBACKD_KEY_THEME)) { + fbd_feedback_manager_load_theme (self); + } else { + g_critical ("Unknown settings key '%s'", key); + } } static void @@ -430,65 +437,6 @@ fbd_feedback_manager_handle_end_feedback (LfbGdbusFeedback *object, return TRUE; } -static const gchar * -find_themefile (void) -{ - gint i = 0; - gsize len; - const gchar *comp; - - g_autoptr (GError) err = NULL; - g_autofree gchar *user_config_path = NULL; - gchar **xdg_data_dirs = (gchar **) g_get_system_data_dirs (); - g_autofree gchar *compatibles = NULL; - - // First look for a default file under $XDG_DATA_HOME - user_config_path = g_build_filename (g_get_user_config_dir (), "feedbackd", - "themes", "default.json", NULL); - if (g_file_test (user_config_path, (G_FILE_TEST_EXISTS))) { - g_debug ("Found user themefile at: %s", user_config_path); - return g_steal_pointer (&user_config_path); - } - - // Try to read the device name - if (g_file_test (DEVICE_TREE_PATH, (G_FILE_TEST_EXISTS))) { - g_debug ("Found device tree device compatible at %s", DEVICE_TREE_PATH); - - // Check if feedbackd has a proper config available this device - if (!g_file_get_contents (DEVICE_TREE_PATH, &compatibles, &len, &err)) - g_warning ("Unable to read: %s", err->message); - - comp = compatibles; - while (comp - compatibles < len) { - - // Iterate over $XDG_DATA_DIRS - for (i = 0; i < g_strv_length (xdg_data_dirs); i++) { - g_autofree gchar *config_path = NULL; - g_autofree gchar *theme_file_name = NULL; - - // We leave it to g_build_filename to add/remove erroneous path separators - theme_file_name = g_strconcat (comp, ".json", NULL); - config_path = g_build_filename (xdg_data_dirs[i], "feedbackd", "themes", - theme_file_name, NULL); - g_debug ("Searching for device specific themefile in %s", config_path); - - // Check if file exist - if (g_file_test (config_path, (G_FILE_TEST_EXISTS))) { - g_debug ("Found themefile for this device at: %s", config_path); - return g_steal_pointer (&config_path); - } - } - - // Next compatible - comp = strchr (comp, 0); - comp++; - } - }else { - g_debug ("Device tree path does not exist: %s", DEVICE_TREE_PATH); - } - - return NULL; -} static void fbd_feedback_manager_constructed (GObject *object) @@ -497,11 +445,11 @@ fbd_feedback_manager_constructed (GObject *object) G_OBJECT_CLASS (fbd_feedback_manager_parent_class)->constructed (object); - fbd_feedback_manager_load_theme(self); - g_signal_connect (self, "notify::profile", (GCallback)on_profile_changed, NULL); self->settings = g_settings_new (FEEDBACKD_SCHEMA_ID); + g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_THEME, + G_CALLBACK (on_feedbackd_setting_changed), self); g_signal_connect_swapped (self->settings, "changed::" FEEDBACKD_KEY_PROFILE, G_CALLBACK (on_feedbackd_setting_changed), self); on_feedbackd_setting_changed (self, FEEDBACKD_KEY_PROFILE, self->settings); @@ -599,31 +547,35 @@ fbd_feedback_manager_get_dev_leds (FbdFeedbackManager *self) return self->leds; } -void fbd_feedback_manager_load_theme (FbdFeedbackManager *self) { - g_autoptr (GError) err = NULL; +void +fbd_feedback_manager_load_theme (FbdFeedbackManager *self) +{ + g_autoptr (FbdThemeExpander) expander = NULL; g_autoptr (FbdFeedbackTheme) theme = NULL; - const gchar *themefile; + g_autoptr (GError) err = NULL; + g_auto (GStrv) compatibles = NULL; + g_autofree char *theme_name = NULL; + const char *theme_file = g_getenv (FEEDBACKD_THEME_VAR); - // Overide themefile with environment variable if requested - themefile = g_getenv (FEEDBACKD_THEME_VAR); + compatibles = gm_devicetree_get_compatibles (NULL, &err); + if (compatibles == NULL && err) { + g_debug ("Failed to get compatibles: %s", err->message); + g_clear_error (&err); + } - // Search for device-specific configuration - if (!themefile) - themefile = find_themefile (); + if (theme_file == NULL) + theme_name = g_settings_get_string (self->settings, FEEDBACKD_KEY_THEME); - // Fallback to default configuration if needed - if (!themefile) - themefile = FEEDBACKD_THEME_DIR "/default.json"; - g_info ("Using themefile: %s", themefile); - - theme = fbd_feedback_theme_new_from_file (themefile, &err); + expander = fbd_theme_expander_new ((const char *const *)compatibles, + theme_name, theme_file); + theme = fbd_theme_expander_load_theme_files (expander, &err); if (theme) { g_set_object(&self->theme, theme); } else { if (self->theme) g_warning ("Failed to reload theme: %s", err->message); else - g_error ("Failed to load theme: %s", err->message); // No point to carry on + g_error ("Failed to load any theme: %s", err->message); // No point to carry on } } diff --git a/src/fbd-feedback-profile.c b/src/fbd-feedback-profile.c index 1ad4f34..28d6f29 100644 --- a/src/fbd-feedback-profile.c +++ b/src/fbd-feedback-profile.c @@ -35,7 +35,7 @@ typedef struct _FbdFeedbackProfile { GObject parent; gchar *name; - GHashTable *feedbacks; + GHashTable *feedbacks; /* key: event name, value: feedback */ } FbdFeedbackProfile; static void json_serializable_iface_init (JsonSerializableIface *iface); @@ -341,3 +341,33 @@ fbd_feedback_profile_level_to_string (FbdFeedbackProfileLevel level) return "full"; } } + +/** + * fbd_profile_update: + * @self: The profile that should be updated + * @new: The profile theme we want to take new feedbacks from + * + * Updates a profile. Feedbacks are read from the `new` profile. If + * feedback already exists in `self` it is overwritten with the feedback + * from `new`. + * + * It is not allowed to update a profile with a profile of a different name. + */ +void +fbd_feedback_profile_update (FbdFeedbackProfile *self, FbdFeedbackProfile *new) +{ + GHashTableIter iter; + const char *event_name; + FbdFeedbackBase *fb; + + g_return_if_fail (FBD_IS_FEEDBACK_PROFILE (self)); + g_return_if_fail (FBD_IS_FEEDBACK_PROFILE (new)); + + g_return_if_fail (g_str_equal (fbd_feedback_profile_get_name (self), + fbd_feedback_profile_get_name (new))); + + g_hash_table_iter_init (&iter, new->feedbacks); + while (g_hash_table_iter_next (&iter, (gpointer)&event_name, (gpointer)&fb)) { + g_hash_table_insert (self->feedbacks, g_strdup (event_name), g_object_ref (fb)); + } +} diff --git a/src/fbd-feedback-profile.h b/src/fbd-feedback-profile.h index 9844172..b054368 100644 --- a/src/fbd-feedback-profile.h +++ b/src/fbd-feedback-profile.h @@ -24,6 +24,8 @@ typedef enum _FbdFeedbackProfileLevel { G_DECLARE_FINAL_TYPE (FbdFeedbackProfile, fbd_feedback_profile, FBD, FEEDBACK_PROFILE, GObject); FbdFeedbackProfile *fbd_feedback_profile_new (const gchar *name); +void fbd_feedback_profile_update (FbdFeedbackProfile *self, + FbdFeedbackProfile *new); const gchar *fbd_feedback_profile_get_name (FbdFeedbackProfile *self); void fbd_feedback_profile_add_feedback (FbdFeedbackProfile *self, FbdFeedbackBase *feedback); diff --git a/src/fbd-feedback-theme.c b/src/fbd-feedback-theme.c index 013615e..6d1bc22 100644 --- a/src/fbd-feedback-theme.c +++ b/src/fbd-feedback-theme.c @@ -18,6 +18,7 @@ enum { PROP_0, PROP_NAME, + PROP_PARENT_NAME, PROP_PROFILES, PROP_LAST_PROP, }; @@ -27,6 +28,8 @@ typedef struct _FbdFeedbackTheme { GObject parent; char *name; + char *parent_name; + GHashTable *profiles; } FbdFeedbackTheme; @@ -115,16 +118,18 @@ fbd_theme_serializable_deserialize_property (JsonSerializable *serializable, static void fbd_feedback_theme_set_property (GObject *object, - guint property_id, - const GValue *value, - GParamSpec *pspec) + guint property_id, + const GValue *value, + GParamSpec *pspec) { FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); switch (property_id) { case PROP_NAME: - g_free (self->name); - self->name = g_value_dup_string (value); + fbd_feedback_theme_set_name (self, g_value_get_string (value)); + break; + case PROP_PARENT_NAME: + fbd_feedback_theme_set_parent_name (self, g_value_get_string (value)); break; case PROP_PROFILES: if (self->profiles) @@ -140,15 +145,18 @@ fbd_feedback_theme_set_property (GObject *object, static void fbd_feedback_theme_get_property (GObject *object, - guint property_id, - GValue *value, - GParamSpec *pspec) + guint property_id, + GValue *value, + GParamSpec *pspec) { FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); switch (property_id) { case PROP_NAME: - g_value_set_string (value, self->name); + g_value_set_string (value, fbd_feedback_theme_get_name (self)); + break; + case PROP_PARENT_NAME: + g_value_set_string (value, fbd_feedback_theme_get_parent_name (self)); break; case PROP_PROFILES: g_value_set_boxed (value, self->profiles); @@ -186,20 +194,6 @@ json_serializable_iface_init (JsonSerializableIface *iface) iface->deserialize_property = fbd_theme_serializable_deserialize_property; } -static void -fbd_feedback_theme_constructed (GObject *object) -{ - FbdFeedbackTheme *self = FBD_FEEDBACK_THEME (object); - - G_OBJECT_CLASS (fbd_feedback_theme_parent_class)->constructed (object); - - self->profiles = g_hash_table_new_full (g_str_hash, - g_str_equal, - g_free, - (GDestroyNotify)g_object_unref); -} - - static void fbd_feedback_theme_class_init (FbdFeedbackThemeClass *klass) { @@ -208,7 +202,6 @@ fbd_feedback_theme_class_init (FbdFeedbackThemeClass *klass) object_class->set_property = fbd_feedback_theme_set_property; object_class->get_property = fbd_feedback_theme_get_property; - object_class->constructed = fbd_feedback_theme_constructed; object_class->dispose = fbd_feedback_theme_dispose; object_class->finalize = fbd_feedback_theme_finalize; @@ -218,7 +211,15 @@ fbd_feedback_theme_class_init (FbdFeedbackThemeClass *klass) "Name", "The theme name", NULL, - G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_PARENT_NAME] = + g_param_spec_string ( + "parent-name", + "Parent theme Name", + "The parent theme name", + NULL, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); props[PROP_PROFILES] = g_param_spec_boxed ( @@ -235,6 +236,10 @@ fbd_feedback_theme_class_init (FbdFeedbackThemeClass *klass) static void fbd_feedback_theme_init (FbdFeedbackTheme *self) { + self->profiles = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify)g_object_unref); } FbdFeedbackTheme * @@ -268,6 +273,20 @@ fbd_feedback_theme_new_from_file (const gchar *filename, GError **error) return fbd_feedback_theme_new_from_data (data, error); } +void +fbd_feedback_theme_set_name (FbdFeedbackTheme *self, const char *name) +{ + g_return_if_fail (FBD_IS_FEEDBACK_THEME (self)); + + if (g_strcmp0 (self->name, name) == 0) + return; + + g_free (self->name); + self->name = g_strdup (name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]); +} + const char * fbd_feedback_theme_get_name (FbdFeedbackTheme *self) { @@ -276,6 +295,31 @@ fbd_feedback_theme_get_name (FbdFeedbackTheme *self) return self->name; } + +void +fbd_feedback_theme_set_parent_name (FbdFeedbackTheme *self, const char *parent_name) +{ + g_return_if_fail (FBD_IS_FEEDBACK_THEME (self)); + + if (g_strcmp0 (self->parent_name, parent_name) == 0) + return; + + g_free (self->parent_name); + self->parent_name = g_strdup (parent_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARENT_NAME]); +} + + +const char * +fbd_feedback_theme_get_parent_name (FbdFeedbackTheme *self) +{ + g_return_val_if_fail (FBD_IS_FEEDBACK_THEME (self), ""); + + return self->parent_name; +} + + void fbd_feedback_theme_add_profile (FbdFeedbackTheme *self, FbdFeedbackProfile *profile) { @@ -317,3 +361,41 @@ fbd_feedback_theme_lookup_feedback (FbdFeedbackTheme *self, g_debug ("No feedback for event %s", fbd_event_get_event (event)); return feedbacks; } + + +/** + * fbd_feedback_theme_update: + * @self: The feedback theme that should be updated + * @new: The feedback theme we want to take profiles and feedbacks new + * + * Merges two feedback themes. Feedbacks and profiles are read new + * the `new` theme. If feedback already exists in the same profile + * of `self` it is overwritten with the feedback in `new`. + */ +void +fbd_feedback_theme_update (FbdFeedbackTheme *self, FbdFeedbackTheme *new) +{ + GHashTableIter iter; + const char *profile_name; + FbdFeedbackProfile *profile; + + g_return_if_fail (FBD_IS_FEEDBACK_THEME (self)); + g_return_if_fail (FBD_IS_FEEDBACK_THEME (new)); + + fbd_feedback_theme_set_name (self, fbd_feedback_theme_get_name (new)); + + g_hash_table_iter_init (&iter, new->profiles); + while (g_hash_table_iter_next (&iter, (gpointer)&profile_name, (gpointer)&profile)) { + FbdFeedbackProfile *current; + + current = g_hash_table_lookup (self->profiles, profile_name); + if (current == NULL) { + current = fbd_feedback_profile_new (profile_name); + g_hash_table_insert (self->profiles, + g_strdup (profile_name), + current); + } + + fbd_feedback_profile_update (current, profile); + } +} diff --git a/src/fbd-feedback-theme.h b/src/fbd-feedback-theme.h index f9ed3ff..3485eb0 100644 --- a/src/fbd-feedback-theme.h +++ b/src/fbd-feedback-theme.h @@ -21,8 +21,13 @@ G_DECLARE_FINAL_TYPE (FbdFeedbackTheme, fbd_feedback_theme, FBD, FEEDBACK_THEME, FbdFeedbackTheme *fbd_feedback_theme_new (const char *name); FbdFeedbackTheme *fbd_feedback_theme_new_from_data (const gchar *data, GError **error); FbdFeedbackTheme *fbd_feedback_theme_new_from_file (const gchar *filename, GError **error); +void fbd_feedback_theme_update (FbdFeedbackTheme *self, FbdFeedbackTheme *from); const char *fbd_feedback_theme_get_name (FbdFeedbackTheme *self); +void fbd_feedback_theme_set_name (FbdFeedbackTheme *self, const char *name); +void fbd_feedback_theme_set_parent_name (FbdFeedbackTheme *self, + const char *parent_name); +const char *fbd_feedback_theme_get_parent_name (FbdFeedbackTheme *self); void fbd_feedback_theme_add_profile (FbdFeedbackTheme *self, FbdFeedbackProfile *profile); FbdFeedbackProfile *fbd_feedback_theme_get_profile (FbdFeedbackTheme *self, const char *name); diff --git a/src/fbd-feedback-vibra.c b/src/fbd-feedback-vibra.c index 52b6d69..9c4555c 100644 --- a/src/fbd-feedback-vibra.c +++ b/src/fbd-feedback-vibra.c @@ -147,7 +147,8 @@ fbd_feedback_vibra_init (FbdFeedbackVibra *self) { } -guint fbd_feedback_vibra_get_duration (FbdFeedbackVibra *self) +guint +fbd_feedback_vibra_get_duration (FbdFeedbackVibra *self) { FbdFeedbackVibraPrivate *priv; diff --git a/src/fbd-theme-expander.c b/src/fbd-theme-expander.c new file mode 100644 index 0000000..2979e3b --- /dev/null +++ b/src/fbd-theme-expander.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2022 Guido Günther + * + * SPDX-License-Identifier: GPL-3.0+ + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "fbd-theme-expander" + +#include "fbd.h" +#include "fbd-feedback-theme.h" +#include "fbd-theme-expander.h" + +#define DEFAULT_THEME_NAME "default" +#define DEVICE_THEME_NAME "$device" + +#define MAX_THEME_DEPTH 10 + +/** + * SECTION:theme-expander + * @short_description: Feedback theme expander + * @Title: FbdThemeExpander + * + * The theme expander reads themes from disks and expands references + * to other themes + */ + +enum { + PROP_0, + PROP_THEME_NAME, + PROP_THEME_FILE, + PROP_COMPATIBLES, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _FbdThemeExpander { + GObject parent; + + char *theme_name; + char *theme_file; + gboolean device_theme_loaded; + GStrv compatibles; +}; +G_DEFINE_TYPE (FbdThemeExpander, fbd_theme_expander, G_TYPE_OBJECT) + + +static void +fbd_theme_expander_set_compatibles (FbdThemeExpander *self, const char * const * compatibles) +{ + g_return_if_fail (FBD_IS_THEME_EXPANDER (self)); + + g_strfreev (self->compatibles); + self->compatibles = g_strdupv ((GStrv)compatibles); + + /* Make sure we reload the device theme */ + self->device_theme_loaded = FALSE; +} + +static void +fbd_theme_expander_set_theme_name (FbdThemeExpander *self, const char *theme_name) +{ + g_return_if_fail (FBD_IS_THEME_EXPANDER (self)); + + g_free (self->theme_name); + if (theme_name) + self->theme_name = g_strdup (theme_name); + else + self->theme_name = g_strdup (DEFAULT_THEME_NAME); + + /* Make sure we reload the device theme */ + self->device_theme_loaded = FALSE; +} + +static void +fbd_theme_expander_set_theme_file (FbdThemeExpander *self, const char *theme_file) +{ + g_return_if_fail (FBD_IS_THEME_EXPANDER (self)); + + g_free (self->theme_file); + self->theme_file = g_strdup (theme_file); + + /* Make sure we reload the device theme */ + self->device_theme_loaded = FALSE; +} + + +static char * +fbd_theme_expander_find_theme_in_xdg_data (const char *theme_name) +{ + const char * const *xdg_data_dirs = g_get_system_data_dirs (); + g_autofree char *theme_file_name = g_strconcat (theme_name, ".json", NULL); + + for (int i = 0; xdg_data_dirs[i] != NULL; i++) { + g_autofree char *theme_path = NULL; + + theme_path = g_build_filename (xdg_data_dirs[i], "feedbackd", "themes", + theme_file_name, NULL); + g_debug ("Looking for theme file at %s", theme_path); + + /* Check if file exist */ + if (g_file_test (theme_path, (G_FILE_TEST_EXISTS))) { + g_info ("Loading theme file at '%s'", theme_path); + return g_steal_pointer (&theme_path); + } + } + return NULL; +} + + +static char * +fbd_theme_expander_find_device_theme_path (FbdThemeExpander *self, const char *theme_name) +{ + /* Device specific lookup only for default theme */ + if (g_str_equal (theme_name, DEVICE_THEME_NAME) == FALSE && + g_str_equal (theme_name, DEFAULT_THEME_NAME) == FALSE) { + return NULL; + } + + if (self->device_theme_loaded) + return NULL; + + self->device_theme_loaded = TRUE; + + /* no compatibles found */ + if (self->compatibles == NULL) + return NULL; + + for (int i = 0; i < g_strv_length (self->compatibles); i++) { + const char *compatible = self->compatibles[i]; + g_autofree char *theme_path = NULL; + + theme_path = fbd_theme_expander_find_theme_in_xdg_data (compatible); + if (theme_path) { + g_info ("Loading themefile for compatible '%s' at: %s", compatible, theme_path); + return g_steal_pointer (&theme_path); + } + } + + g_debug ("No device theme found"); + return NULL; +} + + +static char * +fbd_theme_expander_find_user_theme_path (const char *theme_name) +{ + g_autofree char *filename = g_strdup_printf ("%s.json", theme_name); + g_autofree char *user_config_path = NULL; + + user_config_path = g_build_filename (g_get_user_config_dir (), "feedbackd", + "themes", filename, NULL); + if (g_file_test (user_config_path, (G_FILE_TEST_EXISTS))) { + g_info ("Found theme file at: %s", user_config_path); + return g_steal_pointer (&user_config_path); + } + + g_debug ("No user theme found for '%s'", theme_name); + return NULL; +} + + +static char * +fbd_theme_expander_find_theme_path (FbdThemeExpander *self, const char *theme_name) +{ + g_autofree char *file_name = NULL; + char *theme_path; + + g_assert (theme_name); + + theme_path = fbd_theme_expander_find_user_theme_path (theme_name); + if (theme_path) + return theme_path; + + theme_path = fbd_theme_expander_find_device_theme_path (self, theme_name); + if (theme_path) + return theme_path; + + if (g_str_equal (theme_name, DEFAULT_THEME_NAME) == FALSE) + g_critical ("Theme '%s' not found, falling back to default theme", theme_name); + + theme_path = fbd_theme_expander_find_theme_in_xdg_data (DEFAULT_THEME_NAME); + if (theme_path) + return theme_path; + + /* Last resort is shipped default config */ + file_name = g_strdup_printf ("%s.json", theme_name); + g_info ("Using theme_file: %s", file_name); + return g_build_filename (FEEDBACKD_THEME_DIR, file_name, NULL); +} + + +static void +fbd_theme_expander_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + FbdThemeExpander *self = FBD_THEME_EXPANDER (object); + + switch (property_id) { + case PROP_THEME_NAME: + fbd_theme_expander_set_theme_name (self, g_value_get_string (value)); + break; + case PROP_THEME_FILE: + fbd_theme_expander_set_theme_file (self, g_value_get_string (value)); + break; + case PROP_COMPATIBLES: + fbd_theme_expander_set_compatibles (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_theme_expander_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + FbdThemeExpander *self = FBD_THEME_EXPANDER (object); + + switch (property_id) { + case PROP_THEME_NAME: + g_value_set_string (value, fbd_theme_expander_get_theme_name (self)); + break; + case PROP_THEME_FILE: + g_value_set_string (value, fbd_theme_expander_get_theme_file (self)); + break; + case PROP_COMPATIBLES: + g_value_set_boxed (value, fbd_theme_expander_get_compatibles (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +fbd_theme_expander_finalize (GObject *object) +{ + FbdThemeExpander *self = FBD_THEME_EXPANDER(object); + + g_clear_pointer (&self->theme_name, g_free); + g_clear_pointer (&self->theme_file, g_free); + g_clear_pointer (&self->compatibles, g_strfreev); + + G_OBJECT_CLASS (fbd_theme_expander_parent_class)->finalize (object); +} + + +static void +fbd_theme_expander_class_init (FbdThemeExpanderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = fbd_theme_expander_get_property; + object_class->set_property = fbd_theme_expander_set_property; + object_class->finalize = fbd_theme_expander_finalize; + + /** + * FbdThemeExpander:theme-name: + * + * Specifies the theme to expand. Assumes `default` if unset. + * + * The `compatibles` defines which device specific theme bits are loaded. + */ + props[PROP_THEME_NAME] = + g_param_spec_string ("theme-name", "", "", + DEFAULT_THEME_NAME, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * FbdThemeExpander:theme-file: + * + * Specifies the theme-file to load an expand. Takes preference over `theme`. + */ + props[PROP_THEME_FILE] = + g_param_spec_string ("theme-file", "", "", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * FbdThemeExpander:compatibles: + * + * Specifies the device types this device is compatible with. The + * device compatbiles to look for. When unset the value is retrieved + * from /sysfs. + * + * The `compatibles` defines which device specific theme bits are loaded. + */ + props[PROP_COMPATIBLES] = + g_param_spec_boxed ("compatibles", "", "", + G_TYPE_STRV, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +fbd_theme_expander_init (FbdThemeExpander *self) +{ +} + + +FbdThemeExpander * +fbd_theme_expander_new (const char * const *compatibles, + const char *theme_name, + const char *theme_file) +{ + return FBD_THEME_EXPANDER (g_object_new (FBD_TYPE_THEME_EXPANDER, + "theme-name", theme_name, + "theme-file", theme_file, + "compatibles", compatibles, + NULL)); +} + + +static void +update_theme (gpointer data, gpointer user_data) +{ + FbdFeedbackTheme *theme = FBD_FEEDBACK_THEME (data); + FbdFeedbackTheme *merged = FBD_FEEDBACK_THEME (user_data); + + g_assert (FBD_IS_FEEDBACK_THEME (theme)); + g_assert (FBD_IS_FEEDBACK_THEME (merged)); + + fbd_feedback_theme_update (merged, theme); +} + + +/** + * fbd_theme_expander_load_theme_files: + * @self: The theme expander + * @err: return location for error or %NULL + * + * Parses a theme recursively taking the theme's `parent-name` relations into + * account as well as the expander's list of `compatibles`. + * + * Returns: (transfer full)(allow-none): The parsed theme or %NULL on error + */ +FbdFeedbackTheme * +fbd_theme_expander_load_theme_files (FbdThemeExpander *self, GError **err) +{ + g_autoqueue (FbdFeedbackTheme) queue = g_queue_new (); + g_autoptr (FbdFeedbackTheme) merged = fbd_feedback_theme_new ("merged-theme"); + g_autoptr (FbdFeedbackTheme) theme = NULL; + g_autofree char *theme_file = NULL; + guint len = 0; + + g_return_val_if_fail (FBD_IS_THEME_EXPANDER (self), NULL); + g_return_val_if_fail (err == NULL || *err == NULL, NULL); + + if (self->theme_file) { + theme_file = g_strdup (self->theme_file); + } else { + theme_file = fbd_theme_expander_find_theme_path (self, self->theme_name); + if (g_strcmp0 (self->theme_file, theme_file)) { + self->theme_file = g_steal_pointer (&theme_file); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_THEME_FILE]); + } + } + + theme = fbd_feedback_theme_new_from_file (self->theme_file, err); + if (theme == NULL) + return NULL; + + /* Build a list of themes */ + while (TRUE) { + g_autofree char *parent_path = NULL; + const char *parent_name, *theme_name; + + if (len > MAX_THEME_DEPTH) { + g_set_error (err, fbd_error_quark(), FBD_ERROR_THEME_EXPAND, "Theme depth exceeded"); + return NULL; + } + + theme_name = fbd_feedback_theme_get_name (theme); + if (theme_name == NULL || theme_name[0] == '\0') { + g_set_error (err, fbd_error_quark(), FBD_ERROR_THEME_EXPAND, + "Theme name of %s can't be empty", self->theme_file); + return NULL; + } + + parent_name = fbd_feedback_theme_get_parent_name (theme); + + if (parent_name && + g_str_equal (fbd_feedback_theme_get_name (theme), DEFAULT_THEME_NAME)) { + g_set_error (err, fbd_error_quark(), FBD_ERROR_THEME_EXPAND, + "Default theme can't specify a parent"); + return NULL; + } + + parent_name = fbd_feedback_theme_get_parent_name (theme); + + g_queue_push_head (queue, g_steal_pointer (&theme)); + + if (parent_name == NULL) + break; + + parent_path = fbd_theme_expander_find_theme_path (self, parent_name); + theme = fbd_feedback_theme_new_from_file (parent_path, err); + if (theme == NULL) + return NULL; + + len++; + } + + /* Merge themes bottom to top */ + g_queue_foreach (queue, update_theme, merged); + + fbd_feedback_theme_set_name (merged, self->theme_name); + return g_steal_pointer (&merged); +} + +const char * +fbd_theme_expander_get_theme_name (FbdThemeExpander *self) +{ + g_return_val_if_fail (FBD_IS_THEME_EXPANDER (self), NULL); + + return self->theme_name; + +} + +const char * +fbd_theme_expander_get_theme_file (FbdThemeExpander *self) +{ + g_return_val_if_fail (FBD_IS_THEME_EXPANDER (self), NULL); + + return self->theme_file; +} + +const char * const * +fbd_theme_expander_get_compatibles (FbdThemeExpander *self) +{ + g_return_val_if_fail (FBD_IS_THEME_EXPANDER (self), NULL); + + return (const char * const*)self->compatibles; +} diff --git a/src/fbd-theme-expander.h b/src/fbd-theme-expander.h new file mode 100644 index 0000000..834907e --- /dev/null +++ b/src/fbd-theme-expander.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 Guido Günther + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "fbd-feedback-theme.h" + +#include + +G_BEGIN_DECLS + +#define FBD_TYPE_THEME_EXPANDER (fbd_theme_expander_get_type ()) + +G_DECLARE_FINAL_TYPE (FbdThemeExpander, fbd_theme_expander, FBD, THEME_EXPANDER, GObject) + + +FbdThemeExpander *fbd_theme_expander_new (const char * const *compatibles, + const char *theme_name, + const char *theme_file); +FbdFeedbackTheme *fbd_theme_expander_load_theme_files (FbdThemeExpander *self, + GError **err); +const char *fbd_theme_expander_get_theme_name (FbdThemeExpander *self); +const char *fbd_theme_expander_get_theme_file (FbdThemeExpander *self); +const char * const *fbd_theme_expander_get_compatibles (FbdThemeExpander *self); + +G_END_DECLS diff --git a/src/fbd.c b/src/fbd.c index 31f20f8..b1bb320 100644 --- a/src/fbd.c +++ b/src/fbd.c @@ -17,19 +17,6 @@ static GMainLoop *loop; - -GQuark -fbd_error_quark (void) -{ - static GQuark quark = 0; - - if (!quark) - quark = g_quark_from_static_string("fbd"); - - return quark; -} - - static gboolean quit_cb (gpointer user_data) { @@ -105,6 +92,7 @@ int main(int argc, char *argv[]) { g_autoptr(GError) err = NULL; g_autoptr(GOptionContext) opt_context = NULL; + g_autoptr (FbdFeedbackManager) manager = NULL; opt_context = g_option_context_new ("- A daemon to trigger event feedback"); if (!g_option_context_parse (opt_context, &argc, &argv, &err)) { @@ -113,6 +101,9 @@ int main(int argc, char *argv[]) return 1; } + manager = fbd_feedback_manager_get_default (); + fbd_feedback_manager_load_theme (manager); + g_unix_signal_add (SIGTERM, quit_cb, NULL); g_unix_signal_add (SIGINT, quit_cb, NULL); g_unix_signal_add (SIGHUP, reload_cb, NULL); @@ -130,6 +121,4 @@ int main(int argc, char *argv[]) g_main_loop_run (loop); g_main_loop_unref (loop); - - g_object_unref (fbd_feedback_manager_get_default ()); } diff --git a/src/fbd.h b/src/fbd.h index c5a1e12..82da4e2 100644 --- a/src/fbd.h +++ b/src/fbd.h @@ -13,6 +13,7 @@ G_BEGIN_DECLS typedef enum { FBD_ERROR_FAILED = 0, + FBD_ERROR_THEME_EXPAND = 1, } FbdError; GQuark fbd_error_quark (void); diff --git a/src/meson.build b/src/meson.build index 918c73f..4a2db09 100644 --- a/src/meson.build +++ b/src/meson.build @@ -12,7 +12,7 @@ fbd_enum_sources = gnome.mkenums_simple('fbd-enums', sources = [ generated_dbus_sources, fbd_enum_sources, - 'fbd.c', + 'fbd-error.c', 'fbd-dev-vibra.c', 'fbd-dev-sound.c', 'fbd-dev-leds.c', @@ -27,6 +27,7 @@ sources = [ 'fbd-feedback-vibra.c', 'fbd-feedback-vibra-periodic.c', 'fbd-feedback-vibra-rumble.c', + 'fbd-theme-expander.c', 'fbd-udev.c', ] @@ -34,6 +35,7 @@ fbd_deps = [ gio, gio_unix, glib, + gmobile, gobject, gsound, gudev, diff --git a/subprojects/gmobile b/subprojects/gmobile new file mode 160000 index 0000000..1039e78 --- /dev/null +++ b/subprojects/gmobile @@ -0,0 +1 @@ +Subproject commit 1039e7808195d4de367ce2718481641ca8af2427 diff --git a/tests/data/parent/base.json b/tests/data/parent/base.json new file mode 100644 index 0000000..650d625 --- /dev/null +++ b/tests/data/parent/base.json @@ -0,0 +1,30 @@ +{ + "name" : "base", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-00", + "duration" : 0x01 + }, + { + "type" : "Dummy", + "event-name" : "test-dummy-01", + "duration" : 0x02 + } + ] + }, + { + "name" : "quiet", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-00", + "duration" : 0x03 + } + ] + } + ] +} diff --git a/tests/data/parent/test.json b/tests/data/parent/test.json new file mode 100644 index 0000000..60f863e --- /dev/null +++ b/tests/data/parent/test.json @@ -0,0 +1,40 @@ +{ + "name" : "test", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-00", + "duration" : 0x10 + }, + { + "type" : "Dummy", + "event-name" : "test-dummy-10", + "duration" : 0x20 + } + ] + }, + { + "name" : "quiet", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-10", + "duration" : 0x30 + } + ] + }, + { + "name" : "silent", + "feedbacks" : [ + { + "type" : "Dummy", + "event-name" : "test-dummy-10", + "duration" : 0x40 + } + ] + } + ] +} diff --git a/tests/data/user-config/feedbackd/themes/custom.json b/tests/data/user-config/feedbackd/themes/custom.json new file mode 100644 index 0000000..d208710 --- /dev/null +++ b/tests/data/user-config/feedbackd/themes/custom.json @@ -0,0 +1,21 @@ +{ + "name" : "custom", + "parent-name": "default", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "event-name" : "test-dummy-0", + "type" : "Dummy", + "duration" : 0x10 + }, + { + "event-name" : "test-dummy-2", + "type" : "Dummy", + "duration" : 0x30 + } + ] + } + ] +} diff --git a/tests/data/xdg-data/feedbackd/themes/chainup.json b/tests/data/xdg-data/feedbackd/themes/chainup.json new file mode 100644 index 0000000..e0b3278 --- /dev/null +++ b/tests/data/xdg-data/feedbackd/themes/chainup.json @@ -0,0 +1,4 @@ +{ + "name" : "$device", + "parent-name": "default" +} diff --git a/tests/data/xdg-data/feedbackd/themes/default.json b/tests/data/xdg-data/feedbackd/themes/default.json new file mode 100644 index 0000000..0b5d87d --- /dev/null +++ b/tests/data/xdg-data/feedbackd/themes/default.json @@ -0,0 +1,31 @@ +{ + "name" : "default", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "event-name" : "test-dummy-0", + "type" : "Dummy" + }, + { + "event-name" : "test-dummy-1", + "type" : "Dummy" + } + ] + }, + { + "name" : "quiet", + "feedbacks" : [ + { + "event-name" : "test-dummy-0", + "type" : "Dummy" + }, + { + "event-name" : "test-dummy-1", + "type" : "Dummy" + } + ] + } + ] +} diff --git a/tests/data/xdg-data/feedbackd/themes/replace.json b/tests/data/xdg-data/feedbackd/themes/replace.json new file mode 100644 index 0000000..1fc0e26 --- /dev/null +++ b/tests/data/xdg-data/feedbackd/themes/replace.json @@ -0,0 +1,21 @@ +{ + "name" : "$device", + "parent-name": "default", + "profiles" : [ + { + "name" : "full", + "feedbacks" : [ + { + "event-name" : "test-dummy-0", + "type" : "Dummy", + "duration" : 0x10 + }, + { + "event-name" : "test-dummy-2", + "type" : "Dummy", + "duration" : 0x20 + } + ] + } + ] +} diff --git a/tests/meson.build b/tests/meson.build index d9905f5..254f684 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -8,10 +8,10 @@ test_env.set('GSETTINGS_BACKEND', 'memory') test_env.set('GSETTINGS_SCHEMA_DIR', '@0@/data'.format(meson.project_build_root())) test_env.set('PYTHONDONTWRITEBYTECODE', 'yes') test_env.set('MALLOC_CHECK_', '2') -test_env.set('XDG_CONFIG_HOME', meson.current_source_dir()) +test_env.set('XDG_CONFIG_HOME', meson.current_source_dir() / 'data' / 'user-config') test_env.set('XDG_CONFIG_DIRS', meson.current_source_dir()) -# Shared lfbrary Unit tests +# Shared library Unit tests test_lfb_cflags = [ '-DTEST_APP_ID="org.sigxcpu.feedbackd_test"', @@ -52,9 +52,13 @@ endforeach # Daemon Unit tests if get_option('daemon') +test_env_fbd = test_env +test_env_fbd.set('XDG_DATA_DIRS', meson.current_source_dir() / 'data' / 'xdg-data') + test_fbd_cflags = [ - '-DTEST_APP_ID="org.sigxcpu.feedbackd_test"', -] + '-DTEST_APP_ID="org.sigxcpu.feedbackd_test"', + '-DTEST_DATA_DIR="@0@"'.format(join_paths(meson.current_source_dir(), 'data')), + ] test_fbd_link_args = [ '-fPIC', @@ -68,6 +72,7 @@ fbd_tests = [ 'fbd-feedback-profile', 'fbd-feedback-theme', 'fbd-event', + 'fbd-theme-expander', ] foreach test : fbd_tests @@ -78,7 +83,7 @@ foreach test : fbd_tests link_args : test_fbd_link_args, include_directories : fbd_inc, dependencies : test_fbd_deps) - test(test, t, env : test_env) + test(test, t, env : test_env_fbd) endforeach diff --git a/tests/test-fbd-feedback-profile.c b/tests/test-fbd-feedback-profile.c index bb7cc5c..96fd247 100644 --- a/tests/test-fbd-feedback-profile.c +++ b/tests/test-fbd-feedback-profile.c @@ -81,6 +81,49 @@ test_fbd_feedback_profile_parse (void) g_assert_true (FBD_IS_FEEDBACK_VIBRA(fb)); } + +static void +test_fbd_feedback_profile_update (void) +{ + FbdFeedbackBase *fb; + FbdFeedbackProfile *a = fbd_feedback_profile_new (PROFILE_NAME); + FbdFeedbackProfile *b = fbd_feedback_profile_new (PROFILE_NAME); + + g_autoptr (FbdFeedbackDummy) fb_a_1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "a-1", + NULL); + g_autoptr (FbdFeedbackDummy) fb_a_2 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "a-2", + NULL); + /* New event */ + g_autoptr (FbdFeedbackDummy) fb_b_1 = g_object_new (FBD_TYPE_FEEDBACK_DUMMY, + "event-name", "b-1", + NULL); + /* overwrite existing event */ + g_autoptr (FbdFeedbackDummy) fb_b_2 = g_object_new (FBD_TYPE_FEEDBACK_VIBRA, + "event-name", "a-2", + NULL); + + fbd_feedback_profile_add_feedback (a, FBD_FEEDBACK_BASE(fb_a_1)); + fbd_feedback_profile_add_feedback (a, FBD_FEEDBACK_BASE(fb_a_2)); + + fbd_feedback_profile_add_feedback (b, FBD_FEEDBACK_BASE(fb_b_1)); + fbd_feedback_profile_add_feedback (b, FBD_FEEDBACK_BASE(fb_b_2)); + + fbd_feedback_profile_update (a,b); + + fb = fbd_feedback_profile_get_feedback (a, "a-1"); + g_assert_true (FBD_IS_FEEDBACK_DUMMY(fb)); + fb = fbd_feedback_profile_get_feedback (a, "a-2"); + g_assert_true (FBD_IS_FEEDBACK_VIBRA(fb)); + fb = fbd_feedback_profile_get_feedback (a, "b-1"); + g_assert_true (FBD_IS_FEEDBACK_DUMMY(fb)); + + g_assert_finalize_object (b); + g_assert_finalize_object (a); +} + + gint main (gint argc, gchar *argv[]) { @@ -89,6 +132,7 @@ main (gint argc, gchar *argv[]) g_test_add_func("/feedbackd/fbd/feedback-profile/name", test_fbd_feedback_profile_name); g_test_add_func("/feedbackd/fbd/feedback-profile/feedbacks", test_fbd_feedback_profile_feedbacks); g_test_add_func("/feedbackd/fbd/feedback-profile/parse", test_fbd_feedback_profile_parse); + g_test_add_func("/feedbackd/fbd/feedback-profile/update", test_fbd_feedback_profile_update); return g_test_run(); } diff --git a/tests/test-fbd-feedback-theme.c b/tests/test-fbd-feedback-theme.c index 9b333f4..c5cb636 100644 --- a/tests/test-fbd-feedback-theme.c +++ b/tests/test-fbd-feedback-theme.c @@ -65,6 +65,7 @@ test_fbd_feedback_theme_parse (void) const char *json =" " "{ " " \"name\" : \"test\", " + " \"parent-name\" : \"parent-test\", " " \"profiles\" : [ " " { " " \"name\" : \"full\", " @@ -97,10 +98,18 @@ test_fbd_feedback_theme_parse (void) g_autoptr (GError) err = NULL; g_autoptr (FbdFeedbackTheme) theme = NULL; FbdFeedbackProfile *profile; + const char *name; theme = fbd_feedback_theme_new_from_data (json, &err); g_assert_no_error (err); g_assert_nonnull (theme); + + name = fbd_feedback_theme_get_name (theme); + g_assert_cmpstr ("test", ==, name); + + name = fbd_feedback_theme_get_parent_name (theme); + g_assert_cmpstr ("parent-test", ==, name); + profile = fbd_feedback_theme_get_profile (theme, "full"); g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); g_assert_cmpstr ("full", ==, @@ -111,6 +120,71 @@ test_fbd_feedback_theme_parse (void) fbd_feedback_profile_get_name (profile)); } + +static void +test_fbd_feedback_theme_update (void) +{ + g_autoptr (GError) err = NULL; + FbdFeedbackTheme *theme, *from; + FbdFeedbackProfile *profile; + FbdFeedbackBase *fb; + const char *name; + + /* test-dummy-1x feedbacks */ + from = fbd_feedback_theme_new_from_file (TEST_DATA_DIR "/parent/test.json", &err); + g_assert_no_error (err); + g_assert_true (FBD_IS_FEEDBACK_THEME (from)); + + /* test-dummy-0{0,1} feedbacks */ + theme = fbd_feedback_theme_new_from_file (TEST_DATA_DIR "/parent/base.json", &err); + g_assert_no_error (err); + g_assert_true (FBD_IS_FEEDBACK_THEME (theme)); + + name = fbd_feedback_theme_get_name (from); + g_assert_cmpstr ("test", ==, name); + + name = fbd_feedback_theme_get_name (theme); + g_assert_cmpstr ("base", ==, name); + + /* `theme` is lacking "silent" while `from` has it */ + profile = fbd_feedback_theme_get_profile (theme, "silent"); + g_assert_null (profile); + profile = fbd_feedback_theme_get_profile (from, "silent"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + + fbd_feedback_theme_update (theme, from); + + /* After merging `theme` has a silent profile too */ + profile = fbd_feedback_theme_get_profile (theme, "silent"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + g_assert_cmpstr ("silent", ==, fbd_feedback_profile_get_name (profile)); + + /* Name updated */ + name = fbd_feedback_theme_get_name (theme); + g_assert_cmpstr ("test", ==, name); + + /* test-dummy-10 added */ + profile = fbd_feedback_theme_get_profile (theme, "full"); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-10"); + g_assert_cmpstr ("test-dummy-10", ==, fbd_feedback_get_event_name (fb)); + profile = fbd_feedback_theme_get_profile (theme, "quiet"); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-10"); + g_assert_cmpstr ("test-dummy-10", ==, fbd_feedback_get_event_name (fb)); + profile = fbd_feedback_theme_get_profile (theme, "silent"); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-10"); + g_assert_cmpstr ("test-dummy-10", ==, fbd_feedback_get_event_name (fb)); + + /* test-dummy-00 updated */ + profile = fbd_feedback_theme_get_profile (theme, "full"); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-00"); + g_assert_cmpstr ("test-dummy-00", ==, fbd_feedback_get_event_name (fb)); + g_assert_cmpint (fbd_feedback_dummy_get_duration (FBD_FEEDBACK_DUMMY (fb)), ==, 0x10); + + g_assert_finalize_object (from); + g_assert_finalize_object (theme); +} + + gint main (gint argc, gchar *argv[]) { @@ -119,6 +193,7 @@ main (gint argc, gchar *argv[]) g_test_add_func("/feedbackd/fbd/feedback-theme/name", test_fbd_feedback_theme_name); g_test_add_func("/feedbackd/fbd/feedback-theme/profiles", test_fbd_feedback_theme_profiles); g_test_add_func("/feedbackd/fbd/feedback-theme/parse", test_fbd_feedback_theme_parse); + g_test_add_func("/feedbackd/fbd/feedback-theme/update", test_fbd_feedback_theme_update); return g_test_run(); } diff --git a/tests/test-fbd-theme-expander.c b/tests/test-fbd-theme-expander.c new file mode 100644 index 0000000..accb2ae --- /dev/null +++ b/tests/test-fbd-theme-expander.c @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 Guido Günther + * + * SPDX-License-Identifier: GPL-3.0+ + */ + +#include "fbd-feedback-dummy.h" +#include "fbd-theme-expander.h" + +#include + + +static void +test_fbd_theme_expander_object (void) +{ + g_autoptr (FbdThemeExpander) expander = g_object_new (FBD_TYPE_THEME_EXPANDER, NULL); + + g_assert_cmpstr ("default", ==, fbd_theme_expander_get_theme_name (expander)); + g_assert_null (fbd_theme_expander_get_theme_file (expander)); + g_assert_null (fbd_theme_expander_get_compatibles (expander)); +} + +static void +test_fbd_theme_expander_device (void) +{ + g_autoptr (GError) err = NULL; + const char *compatibles[] = { NULL, NULL }; + FbdFeedbackProfile *profile; + FbdThemeExpander *expander; + FbdFeedbackTheme *theme; + FbdFeedbackBase *fb; + const char *const *compatibles2; + + /* just the default profile */ + compatibles[0] = "doesnotexist"; + expander = fbd_theme_expander_new (compatibles, NULL, NULL); + g_assert_cmpstr ("default", ==, fbd_theme_expander_get_theme_name (expander)); + + theme = fbd_theme_expander_load_theme_files (expander, &err); + compatibles2 = fbd_theme_expander_get_compatibles (expander); +#if GLIB_CHECK_VERSION(2, 68, 0) + g_assert_cmpstrv (compatibles2, compatibles); +#endif + g_assert_no_error (err); + profile = fbd_feedback_theme_get_profile (theme, "silent"); + g_assert_null (profile); + profile = fbd_feedback_theme_get_profile (theme, "quiet"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + g_assert_finalize_object (theme); + g_assert_finalize_object (expander); + + /* Empty device theme that just chains up */ + compatibles[0] = "chainup"; + expander = fbd_theme_expander_new (compatibles, NULL, NULL); + compatibles2 = fbd_theme_expander_get_compatibles (expander); +#if GLIB_CHECK_VERSION(2, 68, 0) + g_assert_cmpstrv (compatibles2, compatibles); +#endif + theme = fbd_theme_expander_load_theme_files (expander, &err); + g_assert_no_error (err); + profile = fbd_feedback_theme_get_profile (theme, "silent"); + g_assert_null (profile); + profile = fbd_feedback_theme_get_profile (theme, "quiet"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + g_assert_finalize_object (theme); + g_assert_finalize_object (expander); + + /* Replace an existing event */ + compatibles[0] = "replace"; + expander = fbd_theme_expander_new (compatibles, NULL, NULL); + compatibles2 = fbd_theme_expander_get_compatibles (expander); +#if GLIB_CHECK_VERSION(2, 68, 0) + g_assert_cmpstrv (compatibles2, compatibles); +#endif + theme = fbd_theme_expander_load_theme_files (expander, &err); + g_assert_no_error (err); + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-0"); + g_assert_cmpstr ("test-dummy-0", ==, fbd_feedback_get_event_name (fb)); + g_assert_cmpint (fbd_feedback_dummy_get_duration (FBD_FEEDBACK_DUMMY (fb)), ==, 0x10); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-2"); + g_assert_cmpstr ("test-dummy-2", ==, fbd_feedback_get_event_name (fb)); + g_assert_cmpint (fbd_feedback_dummy_get_duration (FBD_FEEDBACK_DUMMY (fb)), ==, 0x20); + g_assert_finalize_object (theme); + g_assert_finalize_object (expander); +} + +static void +test_fbd_theme_expander_custom (void) +{ + g_autoptr (GError) err = NULL; + const char *compatibles[] = { NULL, NULL }; + FbdFeedbackProfile *profile; + FbdThemeExpander *expander; + FbdFeedbackTheme *theme; + FbdFeedbackBase *fb; + + /* Load a custom theme that overrides a single event on top of the `replace` from above */ + compatibles[0] = "replace"; + expander = fbd_theme_expander_new (compatibles, "custom", NULL); + theme = fbd_theme_expander_load_theme_files (expander, &err); + g_assert_no_error (err); + profile = fbd_feedback_theme_get_profile (theme, "full"); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-0"); + g_assert_cmpstr ("test-dummy-0", ==, fbd_feedback_get_event_name (fb)); + g_assert_cmpint (fbd_feedback_dummy_get_duration (FBD_FEEDBACK_DUMMY (fb)), ==, 0x10); + g_assert_true (FBD_IS_FEEDBACK_PROFILE(profile)); + fb = fbd_feedback_profile_get_feedback (profile, "test-dummy-2"); + g_assert_cmpstr ("test-dummy-2", ==, fbd_feedback_get_event_name (fb)); + g_assert_cmpint (fbd_feedback_dummy_get_duration (FBD_FEEDBACK_DUMMY (fb)), ==, 0x30); + g_assert_finalize_object (theme); + g_assert_finalize_object (expander); +} + +gint +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func("/feedbackd/fbd/theme-expander/object", test_fbd_theme_expander_object); + g_test_add_func("/feedbackd/fbd/theme-expander/device", test_fbd_theme_expander_device); + g_test_add_func("/feedbackd/fbd/theme-expander/custom", test_fbd_theme_expander_custom); + + return g_test_run(); +}