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();
+}