Files
feedbackd/src/fbd-theme-expander.c
T
Guido Günther 242519498a Recursively load themes
Themes (other than the default theme) can specify a `parent-name` in the
json file. This theme will be used as a base for the theme. Events and
profiles of the children override those in the parents.

For this we move the lookup code into it's separate class and also add
tests.

Closes: #38
2022-12-11 17:34:38 +01:00

401 lines
11 KiB
C

/*
* Copyright (C) 2022 Guido Günther
*
* SPDX-License-Identifier: GPL-3.0+
*
* Author: Guido Günther <agx@sigxcpu.org>
*/
#define G_LOG_DOMAIN "fbd-theme-expander"
#include "fbd.h"
#include "fbd-feedback-theme.h"
#include "fbd-theme-expander.h"
#define DEVICE_TREE_PATH "/sys/firmware/devicetree/base/compatible"
#define DEVICE_NAME_MAX 1024
#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_FILE,
PROP_COMPATIBLES,
PROP_LAST_PROP
};
static GParamSpec *props[PROP_LAST_PROP];
struct _FbdThemeExpander {
GObject parent;
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_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_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_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_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-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_file)
{
return FBD_THEME_EXPANDER (g_object_new (FBD_TYPE_THEME_EXPANDER,
"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, DEFAULT_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);
return g_steal_pointer (&merged);
}
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;
}