/* This file is part of darktable, Copyright (C) 2019-2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. darktable is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with darktable. If not, see . */ #include "common/calculator.h" #include "common/darktable.h" #include "common/file_location.h" #include "common/math.h" #include "control/conf.h" #include #include #include #include #include #include #include typedef struct dt_conf_dreggn_t { GSList *result; const char *match; } dt_conf_dreggn_t; /** return slot for this variable or newly allocated slot. */ static inline char *_conf_get_var(const char *name) { char *str; dt_pthread_mutex_lock(&darktable.conf->mutex); str = (char *)g_hash_table_lookup(darktable.conf->override_entries, name); if(str) goto fin; str = (char *)g_hash_table_lookup(darktable.conf->table, name); if(str) goto fin; // not found, try defaults str = (char *)dt_confgen_get(name, DT_DEFAULT); if(str) { char *str_new = g_strdup(str); g_hash_table_insert(darktable.conf->table, g_strdup(name), str_new); str = str_new; goto fin; } // FIXME: why insert garbage? // still no luck? insert a garbage string with length=0: str = (char *)g_malloc0(sizeof(int32_t)); g_hash_table_insert(darktable.conf->table, g_strdup(name), str); fin: dt_pthread_mutex_unlock(&darktable.conf->mutex); return str; } /* set the value only if it hasn't been overridden from commandline * return 1 if key/value is still the one passed on commandline. */ static int _conf_set_if_not_overridden(const char *name, char *str) { dt_pthread_mutex_lock(&darktable.conf->mutex); char *over = (char *)g_hash_table_lookup(darktable.conf->override_entries, name); const int is_overridden = (over && !strcmp(str, over)); if(!is_overridden) { g_hash_table_insert(darktable.conf->table, g_strdup(name), str); } dt_pthread_mutex_unlock(&darktable.conf->mutex); return is_overridden; } void dt_conf_set_int(const char *name, const int val) { char *str = g_strdup_printf("%d", val); if(_conf_set_if_not_overridden(name, str)) g_free(str); } void dt_conf_set_int64(const char *name, const int64_t val) { char *str = g_strdup_printf("%" PRId64, val); if(_conf_set_if_not_overridden(name, str)) g_free(str); } void dt_conf_set_float(const char *name, const float val) { char *str = (char *)g_malloc(G_ASCII_DTOSTR_BUF_SIZE); g_ascii_dtostr(str, G_ASCII_DTOSTR_BUF_SIZE, val); if(_conf_set_if_not_overridden(name, str)) g_free(str); } void dt_conf_set_bool(const char *name, const int val) { char *str = g_strdup(val ? "TRUE" : "FALSE"); if(_conf_set_if_not_overridden(name, str)) g_free(str); } void dt_conf_set_string(const char *name, const char *val) { char *str = g_strdup(val); if(_conf_set_if_not_overridden(name, str)) g_free(str); } void dt_conf_set_folder_from_file_chooser(const char *name, GtkFileChooser *chooser) { #ifdef WIN32 // for Windows native file chooser, gtk_file_chooser_get_current_folder() // does not work, so we workaround if(GTK_IS_FILE_CHOOSER_NATIVE(chooser)) { gchar *pathname = gtk_file_chooser_get_filename(chooser); if(pathname) { gchar *folder = g_path_get_dirname(pathname); if(_conf_set_if_not_overridden(name, folder)) g_free(folder); g_free(pathname); } return; } #endif gchar *folder = gtk_file_chooser_get_current_folder(chooser); if(_conf_set_if_not_overridden(name, folder)) g_free(folder); } static inline gboolean _conf_isnan(const double val) { return val != val; } static int _conf_get_int_fast(const char *name) { const char *str = _conf_get_var(name); double new_value = dt_calculator_solve(1, str); if(_conf_isnan(new_value)) { //we've got garbage, check default const char *def_val = dt_confgen_get(name, DT_DEFAULT); if(def_val) { new_value = dt_calculator_solve(1, def_val); if(_conf_isnan(new_value)) new_value = 0.0; else { char *fix_badval = g_strdup(def_val); if(_conf_set_if_not_overridden(name, fix_badval)) g_free(fix_badval); } } else { new_value = 0.0; } } int val; if(new_value > 0) val = new_value + 0.5; else val = new_value - 0.5; return val; } int dt_conf_get_int(const char *name) { const int min = dt_confgen_get_int(name, DT_MIN); const int max = dt_confgen_get_int(name, DT_MAX); const int val = _conf_get_int_fast(name); const int ret = CLAMP(val, min, max); return ret; } static int64_t _conf_get_int64_fast(const char *name) { const char *str = _conf_get_var(name); double new_value = dt_calculator_solve(1, str); if(_conf_isnan(new_value)) { //we've got garbage, check default const char *def_val = dt_confgen_get(name, DT_DEFAULT); if(def_val) { new_value = dt_calculator_solve(1, def_val); if(_conf_isnan(new_value)) new_value = 0.0; else { char *fix_badval = g_strdup(def_val); if(_conf_set_if_not_overridden(name, fix_badval)) g_free(fix_badval); } } else { new_value = 0.0; } } int64_t val; if(new_value > 0) val = new_value + 0.5; else val = new_value - 0.5; return val; } int64_t dt_conf_get_int64(const char *name) { const int64_t min = dt_confgen_get_int64(name, DT_MIN); const int64_t max = dt_confgen_get_int64(name, DT_MAX); const int64_t val = _conf_get_int64_fast(name); const int64_t ret = CLAMP(val, min, max); return ret; } float _conf_get_float_fast(const char *name) { const char *str = _conf_get_var(name); double new_value = dt_calculator_solve(1, str); if(_conf_isnan(new_value)) { //we've got garbage, check default const char *def_val = dt_confgen_get(name, DT_DEFAULT); if(def_val) { new_value = dt_calculator_solve(1, def_val); if(_conf_isnan(new_value)) new_value = 0.0; else { char *fix_badval = g_strdup(def_val); if(_conf_set_if_not_overridden(name, fix_badval)) g_free(fix_badval); } } else { new_value = 0.0; } } return (float)new_value; } float dt_conf_get_float(const char *name) { const float min = dt_confgen_get_float(name, DT_MIN); const float max = dt_confgen_get_float(name, DT_MAX); const float val = _conf_get_float_fast(name); const float ret = CLAMP(val, min, max); return ret; } int dt_conf_get_and_sanitize_int(const char *name, const int min, const int max) { const int cmin = dt_confgen_get_int(name, DT_MIN); const int cmax = dt_confgen_get_int(name, DT_MAX); const int val = _conf_get_int_fast(name); const int ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax)); dt_conf_set_int(name, ret); return ret; } int64_t dt_conf_get_and_sanitize_int64(const char *name, const int64_t min, const int64_t max) { const int64_t cmin = dt_confgen_get_int64(name, DT_MIN); const int64_t cmax = dt_confgen_get_int64(name, DT_MAX); const int64_t val = _conf_get_int64_fast(name); const int64_t ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax)); dt_conf_set_int64(name, ret); return ret; } float dt_conf_get_and_sanitize_float(const char *name, const float min, const float max) { const float cmin = dt_confgen_get_float(name, DT_MIN); const float cmax = dt_confgen_get_float(name, DT_MAX); const float val = _conf_get_float_fast(name); const float ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax)); dt_conf_set_float(name, ret); return ret; } gboolean dt_conf_get_bool(const char *name) { const char *str = _conf_get_var(name); return (str[0] != 'F') && (str[0] != 'f') && (str[0] != '0') && (str[0] != '\0'); } void dt_conf_set_path(const char *name, const char *val) { dt_conf_set_string(name, val); } gchar *dt_conf_get_path(const char *name) { const char *path = _conf_get_var(name); const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(path[0] && item && item->type == DT_PATH && !g_file_test(path, G_FILE_TEST_IS_DIR | G_FILE_TEST_IS_SYMLINK)) { path = dt_confgen_get(name, DT_DEFAULT); dt_conf_set_path(name, path); } return g_strdup(path); } gchar *dt_conf_get_string(const char *name) { const char *str = _conf_get_var(name); return g_strdup(str); } const char *dt_conf_get_string_const(const char *name) { return _conf_get_var(name); } gboolean dt_conf_key_not_empty(const char *name) { const char *val = dt_conf_get_string_const(name); return strlen(val) != 0; } gboolean dt_conf_get_folder_to_file_chooser(const char *name, GtkFileChooser *chooser) { const gchar *folder = dt_conf_get_string_const(name); if(folder) { gtk_file_chooser_set_current_folder(chooser, folder); return TRUE; } return FALSE; } gboolean dt_conf_is_equal(const char *name, const char *value) { const char *str = _conf_get_var(name); return g_strcmp0(str, value) == 0; } static char *_sanitize_confgen(const char *name, const char *value) { if(!darktable.conf->x_confgen) return g_strdup(value); const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(!item) return g_strdup(value); char *result = NULL; switch(item->type) { case DT_INT: { double v = dt_calculator_solve(1, value); const int min = item->min ? (int)dt_calculator_solve(1, item->min) : INT_MIN; const int max = item->max ? (int)dt_calculator_solve(1, item->max) : INT_MAX; // if garbage, use default const int val = _conf_isnan(v) ? dt_confgen_get_int(name, DT_DEFAULT) : (int)v; result = g_strdup_printf("%d", CLAMP(val, min, max)); } break; case DT_INT64: { double v = dt_calculator_solve(1, value); const int64_t min = item->min ? (int64_t)dt_calculator_solve(1, item->min) : INT64_MIN; const int64_t max = item->max ? (int64_t)dt_calculator_solve(1, item->max) : INT64_MAX; // if garbage, use default const int64_t val = _conf_isnan(v) ? dt_confgen_get_int64(name, DT_DEFAULT) : (int64_t)v; result = g_strdup_printf("%"PRId64, CLAMP(val, min, max)); } break; case DT_FLOAT: { double v = dt_calculator_solve(1, value); const float min = item->min ? (float)dt_calculator_solve(1, item->min) : -FLT_MAX; const float max = item->max ? (float)dt_calculator_solve(1, item->max) : FLT_MAX; // if garbage, use default const float val = _conf_isnan(v) ? (float)dt_confgen_get_float(name, DT_DEFAULT) : (float)v; result = g_strdup_printf("%f", CLAMP(val, min, max)); } break; case DT_BOOL: { if(strcasecmp(value, "true") && strcasecmp(value, "false")) result = g_strdup(dt_confgen_get(name, DT_DEFAULT)); else result = g_strdup(value); } break; case DT_ENUM: { size_t n = strlen(value); char *v = item->enum_values; while(v++) { if(!g_ascii_strncasecmp(value, v, n) && v[n] == ']') return g_strndup(v, n); v = strchr(v, '['); } result = g_strdup(dt_confgen_get(name, DT_DEFAULT)); } break; default: result = g_strdup(value); break; } return result; } gchar *dt_conf_read_values(const char *filename, gchar* (*callback)(const gchar *key, const gchar *value)) { #define LINE_SIZE 1023 char line[LINE_SIZE + 1]; // check for user config FILE *f = g_fopen(filename, "rb"); // if file has been found, parse it if(f) { while(!feof(f)) { const char* ret = fgets(line, LINE_SIZE, f); if(ret != NULL) { line[strcspn(line, "\r\n")] = 0; char *c = line; char *end = line + strlen(line); // check for '=' which is separator between the conf name and value while(*c != '=' && c < end) c++; if(*c == '=') { *c = '\0'; const char *name = line; // ensure that numbers are properly clamped if min/max // defined and if not and garbage is read then the default // value is returned. char *value = _sanitize_confgen(name, (const char *)(c + 1)); gchar *v = callback(name, value); g_free(value); if(v) return v; } } } fclose(f); } else if(darktable.conf->x_confgen) { // We initialize the conf table with default values. // For common keys, prefer the value already loaded from darktablerc-common // (if present) over the confgen default, so that a freshly-created // darktablerc inherits shared settings rather than resetting them. GHashTableIter iter; gpointer key, value; g_hash_table_iter_init (&iter, darktable.conf->x_confgen); while(g_hash_table_iter_next (&iter, &key, &value)) { const char *name = (const char *)key; if(dt_confgen_is_common(name) && g_hash_table_lookup(darktable.conf->table, name)) continue; const dt_confgen_value_t *entry = (dt_confgen_value_t *)value; gchar *v = callback(name, entry->def); if(v) return v; } } return NULL; } static gchar *_conf_insert_value(const gchar *key, const gchar *value) { g_hash_table_insert(darktable.conf->table, g_strdup(key), g_strdup(value)); return NULL; } void dt_conf_init(dt_conf_t *cf, const char *filename, const gboolean is_common, GSList *override_entries) { // should we initialize the storage if(!cf->table) { cf->table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); cf->override_entries = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); dt_pthread_mutex_init(&darktable.conf->mutex); } // init conf filename if(is_common) { g_strlcpy(cf->filename_common, filename, sizeof(cf->filename_common)); } else { g_strlcpy(cf->filename, filename, sizeof(cf->filename)); } // guard inserting conf values dt_pthread_mutex_lock(&darktable.conf->mutex); dt_conf_read_values(filename, _conf_insert_value); if(override_entries) { for(GSList *p = override_entries; p; p = g_slist_next(p)) { dt_conf_string_entry_t *entry = p->data; g_hash_table_insert(darktable.conf->override_entries, g_strdup(entry->key), g_strdup(entry->value)); } } dt_pthread_mutex_unlock(&darktable.conf->mutex); #undef LINE_SIZE return; } /** check if key exists, return 1 if lookup succeeded, 0 if failed..*/ gboolean dt_conf_key_exists(const char *key) { dt_pthread_mutex_lock(&darktable.conf->mutex); const gboolean res = (g_hash_table_lookup(darktable.conf->table, key) != NULL) || (g_hash_table_lookup(darktable.conf->override_entries, key) != NULL); dt_pthread_mutex_unlock(&darktable.conf->mutex); return (res || dt_confgen_value_exists(key, DT_DEFAULT)); } /** remove key from conf */ void dt_conf_remove_key(const char *key) { dt_pthread_mutex_lock(&darktable.conf->mutex); g_hash_table_remove(darktable.conf->table, key); dt_pthread_mutex_unlock(&darktable.conf->mutex); } static void _conf_add(char *key, char *val, dt_conf_dreggn_t *d) { if(strncmp(key, d->match, strlen(d->match)) == 0) { dt_conf_string_entry_t *nv = g_malloc(sizeof(dt_conf_string_entry_t)); nv->key = g_strdup(key + strlen(d->match) + 1); nv->value = g_strdup(val); d->result = g_slist_append(d->result, nv); } } /** get all strings in */ GSList *dt_conf_all_string_entries(const char *dir) { dt_pthread_mutex_lock(&darktable.conf->mutex); dt_conf_dreggn_t d; d.result = NULL; d.match = dir; g_hash_table_foreach(darktable.conf->table, (GHFunc)_conf_add, &d); dt_pthread_mutex_unlock(&darktable.conf->mutex); return d.result; } void dt_conf_string_entry_free(gpointer data) { dt_conf_string_entry_t *nv = (dt_conf_string_entry_t *)data; g_free(nv->key); g_free(nv->value); nv->key = NULL; nv->value = NULL; g_free(nv); } gboolean dt_confgen_exists(const char *name) { return g_hash_table_lookup(darktable.conf->x_confgen, name) != NULL; } dt_confgen_type_t dt_confgen_type(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item) return item->type; else return DT_STRING; } gboolean dt_confgen_value_exists(const char *name, const dt_confgen_value_kind_t kind) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item == NULL) return FALSE; switch(kind) { case DT_DEFAULT: return item->def != NULL; case DT_MIN: return item->min != NULL; case DT_MAX: return item->max != NULL; case DT_VALUES: return item->enum_values != NULL; } return FALSE; } const char *dt_confgen_get(const char *name, const dt_confgen_value_kind_t kind) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item) { switch(kind) { case DT_DEFAULT: return item->def; case DT_MIN: return item->min; case DT_MAX: return item->max; case DT_VALUES: return item->enum_values; } } return ""; } gboolean dt_confgen_is_common(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item) { return item->is_common; } return FALSE; } const char *dt_confgen_get_label(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item) { return item->shortdesc; } return ""; } const char *dt_confgen_get_tooltip(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); if(item) { return item->longdesc; } return ""; } int dt_confgen_get_int(const char *name, const dt_confgen_value_kind_t kind) { if(!dt_confgen_value_exists(name, kind)) { //early bail switch(kind) { case DT_MIN: return INT_MIN; break; case DT_MAX: return INT_MAX; break; default: return 0; break; } } const char *str = dt_confgen_get(name, kind); //if str is NULL or empty, dt_calculator_solve will return NAN const double value = dt_calculator_solve(1, str); switch(kind) { case DT_MIN: return _conf_isnan(value) ? INT_MIN : (value > 0 ? value + 0.5f : value - 0.5f); break; case DT_MAX: return _conf_isnan(value) ? INT_MAX : (value > 0 ? value + 0.5f : value - 0.5f); break; default: return _conf_isnan(value) ? 0.0f : (value > 0 ? value + 0.5f : value - 0.5f); break; } return (int)value; } int64_t dt_confgen_get_int64(const char *name, const dt_confgen_value_kind_t kind) { if(!dt_confgen_value_exists(name, kind)) { //early bail switch(kind) { case DT_MIN: return INT64_MIN; break; case DT_MAX: return INT64_MAX; break; default: return 0; break; } } const char *str = dt_confgen_get(name, kind); //if str is NULL or empty, dt_calculator_solve will return NAN const double value = dt_calculator_solve(1, str); switch(kind) { case DT_MIN: return _conf_isnan(value) ? INT64_MIN : (value > 0 ? value + 0.5f : value - 0.5f); break; case DT_MAX: return _conf_isnan(value) ? INT64_MAX : (value > 0 ? value + 0.5f : value - 0.5f); break; default: return _conf_isnan(value) ? 0.0f : (value > 0 ? value + 0.5f : value - 0.5f); break; } return (int64_t)value; } gboolean dt_confgen_get_bool(const char *name, const dt_confgen_value_kind_t kind) { const char *str = dt_confgen_get(name, kind); return !strcmp(str, "true"); } float dt_confgen_get_float(const char *name, const dt_confgen_value_kind_t kind) { if(!dt_confgen_value_exists(name, kind)) { //early bail switch(kind) { case DT_MIN: return -FLT_MAX; break; case DT_MAX: return FLT_MAX; break; default: break; } return 0.0f; } const char *str = dt_confgen_get(name, kind); //if str is NULL or empty, dt_calculator_solve will return NAN const double value = dt_calculator_solve(1, str); switch(kind) { case DT_MIN: // to anyone askig FLT_MIN is superclose to 0, not furthest value // from 0 possible in float return _conf_isnan(value) ? -FLT_MAX : (float)value; break; case DT_MAX: return _conf_isnan(value) ? FLT_MAX : (float)value; break; default: break; } return _conf_isnan(value) ? 0.0f : (float)value; } gboolean dt_conf_is_default(const char *name) { if(!dt_confgen_exists(name)) return TRUE; // well if default doesn't know about it, it's default switch(dt_confgen_type(name)) { case DT_INT: return dt_conf_get_int(name) == dt_confgen_get_int(name, DT_DEFAULT); break; case DT_INT64: return dt_conf_get_int64(name) == dt_confgen_get_int64(name, DT_DEFAULT); break; case DT_FLOAT: return dt_conf_get_float(name) == dt_confgen_get_float(name, DT_DEFAULT); break; case DT_BOOL: return dt_conf_get_bool(name) == dt_confgen_get_bool(name, DT_DEFAULT); break; case DT_PATH: case DT_STRING: case DT_ENUM: default: { const char *def_val = dt_confgen_get(name, DT_DEFAULT); const char *cur_val = _conf_get_var(name); return g_strcmp0(def_val, cur_val) == 0; break; } } } gchar* dt_conf_expand_default_dir(const char *dir) { // expand special dirs #define CONFIG_DIR "$(config)" #define HOME_DIR "$(home)" gchar *path = NULL; if(g_str_has_prefix(dir, CONFIG_DIR)) { gchar configdir[PATH_MAX] = { 0 }; dt_loc_get_user_config_dir(configdir, sizeof(configdir)); path = g_strdup_printf("%s%s", configdir, dir + strlen(CONFIG_DIR)); } else if(g_str_has_prefix(dir, HOME_DIR)) { gchar *homedir = dt_loc_get_home_dir(NULL); path = g_strdup_printf("%s%s", homedir, dir + strlen(HOME_DIR)); g_free(homedir); } else path = g_strdup(dir); gchar *normalized_path = dt_util_normalize_path(path); g_free(path); return normalized_path; } static void _conf_print(const gchar *key, const gchar *val, FILE *f) { fprintf(f, "%s=%s\n", key, val); } static void _conf_save(dt_conf_t *cf) { FILE *fc = g_fopen(cf->filename_common, "wb"); FILE *fs = g_fopen(cf->filename, "wb"); if(fs && fc) { GList *keys = g_hash_table_get_keys(cf->table); GList *sorted = g_list_sort(keys, (GCompareFunc)g_strcmp0); for(GList *iter = sorted; iter; iter = g_list_next(iter)) { const gchar *key = (const gchar *)iter->data; const gchar *val = (const gchar *)g_hash_table_lookup(cf->table, key); FILE *f = dt_confgen_is_common(key) ? fc : fs; _conf_print(key, val, f); } g_list_free(sorted); fclose(fs); fclose(fc); } } void dt_conf_cleanup(dt_conf_t *cf) { _conf_save(cf); g_hash_table_unref(cf->table); g_hash_table_unref(cf->override_entries); g_hash_table_unref(cf->x_confgen); dt_pthread_mutex_destroy(&darktable.conf->mutex); } // ── welcome-screen metadata ─────────────────────────────────────────────────── int dt_confgen_get_welcome_pagenum(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); return item ? item->welcome_pagenum : 0; } gboolean dt_confgen_get_welcome_dirchooser(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); return item ? item->welcome_dirchooser : FALSE; } const char *const *dt_confgen_get_welcome_options(const char *name) { const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name); return item ? (const char *const *)item->welcome_options : NULL; } static gint _welcome_key_compare(gconstpointer a, gconstpointer b) { const dt_confgen_value_t *ea = g_hash_table_lookup(darktable.conf->x_confgen, (const char *)a); const dt_confgen_value_t *eb = g_hash_table_lookup(darktable.conf->x_confgen, (const char *)b); if(!ea || !eb) return 0; if(ea->welcome_pagenum != eb->welcome_pagenum) return ea->welcome_pagenum - eb->welcome_pagenum; return ea->welcome_questionnum - eb->welcome_questionnum; } GList *dt_confgen_get_welcome_keys(void) { GList *result = NULL; GHashTableIter iter; gpointer key, value; g_hash_table_iter_init(&iter, darktable.conf->x_confgen); while(g_hash_table_iter_next(&iter, &key, &value)) { const dt_confgen_value_t *entry = value; if(entry->welcome_pagenum > 0) result = g_list_prepend(result, g_strdup((const char *)key)); } result = g_list_sort(result, _welcome_key_compare); // validate page and question numbering: pages must start at 1 with no gaps; // questions on each page must start at 1 with no gaps. int expected_page = 1; int expected_question = 1; int cur_page = 0; for(GList *l = result; l; l = l->next) { const char *k = l->data; const dt_confgen_value_t *e = g_hash_table_lookup(darktable.conf->x_confgen, k); if(!e) continue; if(e->welcome_pagenum != cur_page) { if(e->welcome_pagenum != expected_page) fprintf(stderr, "[confgen] welcome screen: expected page %d but got page %d (key: %s)\n", expected_page, e->welcome_pagenum, k); cur_page = e->welcome_pagenum; expected_page = cur_page + 1; expected_question = 1; } if(e->welcome_questionnum != expected_question) fprintf(stderr, "[confgen] welcome screen: on page %d expected question %d but got %d (key: %s)\n", cur_page, expected_question, e->welcome_questionnum, k); expected_question = e->welcome_questionnum + 1; } return result; } // clang-format off // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py // vim: shiftwidth=2 expandtab tabstop=2 cindent // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; // clang-format on