brewery
notashelf /
40227627c1a3b0945213400878d0933308e8d985

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit 40227627c1a3

tarball

NotAShelf <raf@notashelf.dev> · 2026-05-01 10:10 UTC

unverified2 files changed+209-390
config: drop custom INI parser; migrate to TOML configurations

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6eabda96988b987d7397d6fc3cd47f2f6a6a6964
diff --git a/Makefile b/Makefileindex d293bdc..c1bffd4 100644--- a/Makefile+++ b/Makefile@@ -1,5 +1,5 @@ PROJECT_NAME = chroma-VERSION = 1.1.0+VERSION = 2.0.0  # Directories SRCDIR = src@@ -14,6 +14,9 @@ PREFIX ?= /usr/local BINDIR_INSTALL = $(PREFIX)/bin SYSTEMD_INSTALL = $(HOME)/.config/systemd/user +# Config file+CONFIG_FILE_NAME = chroma.toml+ # Compiler and flags CC = gcc CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g@@ -46,8 +49,11 @@ LDFLAGS += -lm -ldl  # Source files (excluding generated protocol files) SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))+VENDOR_SOURCES = $(INCDIR)/vendor/tomlc17.c OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)-DEPENDS = $(OBJECTS:.o=.d)+VENDOR_OBJECTS = $(VENDOR_SOURCES:$(INCDIR)/vendor/%.c=$(OBJDIR)/%.o)+ALL_OBJECTS = $(OBJECTS) $(VENDOR_OBJECTS)+DEPENDS = $(ALL_OBJECTS:.o=.d)  # Override object files for image.c and render.c to suppress third-party warnings OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS))@@ -78,9 +84,9 @@ $(INCDIR): 	@mkdir -p $(INCDIR)  # Build main executable-$(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)+$(TARGET): version-header $(PROTOCOL_HEADERS) $(ALL_OBJECTS) | $(BINDIR) 	@echo "  LINK  $@"-	@$(CC) $(OBJECTS) -o $@ $(LDFLAGS)+	@$(CC) $(ALL_OBJECTS) -o $@ $(LDFLAGS)  # Compile source files $(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)@@ -95,6 +101,11 @@ $(OBJDIR)/render.o: $(SRCDIR)/render.c $(PROTOCOL_HEADERS) | $(OBJDIR) 	@echo "  CC    $<" 	@$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@ +# Compile vendor files+$(OBJDIR)/%.o: $(INCDIR)/vendor/%.c | $(OBJDIR)+	@echo "  CC    $<"+	@$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@+ # Debug build debug: CFLAGS = $(DEBUG_CFLAGS) debug: $(TARGET)@@ -145,7 +156,7 @@ systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service # Create sample configuration file sample-config: 	@echo "Creating sample configuration..."-	@cp chroma.conf.sample $(CONFIG_FILE_NAME)+	@cp chroma.toml.sample $(CONFIG_FILE_NAME) 	@echo "Sample configuration created at $(CONFIG_FILE_NAME)"  # Clean build artifactsdiff --git a/src/config.c b/src/config.cindex 0c28132..d082249 100644--- a/src/config.c+++ b/src/config.c@@ -5,29 +5,9 @@ #include <string.h> #include <strings.h> -#include "../include/chroma.h"+#include "chroma.h"+#include "vendor/tomlc17.h" -static char *trim_whitespace(char *str) {-  char *end;--  // Trim leading whitespace-  while (isspace((unsigned char)*str))-    str++;--  // All spaces?-  if (*str == 0)-    return str;--  // Trim trailing whitespace-  end = str + strlen(str) - 1;-  while (end > str && isspace((unsigned char)*end))-    end--;--  *(end + 1) = '\0';-  return str;-}--// Remove quotes from a string // Match output name/description against a config pattern // Supports: //   - Exact name match: "DP-1" matches wl_output.name == "DP-1"@@ -60,31 +40,8 @@ static bool match_output(const char *pattern, const char *output_name,   return false; } -static char *remove_quotes(char *str) {-  size_t len = strlen(str);-  if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||-                   (str[0] == '\'' && str[len - 1] == '\''))) {-    str[len - 1] = '\0';-    return str + 1;-  }-  return str;-}--// Parse boolean value from string-static bool parse_bool(const char *value) {-  if (!value)-    return false;--  if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||-      strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {-    return true;-  }--  return false;-}- // Parse scaling mode from string-static chroma_scale_mode_t parse_scale_mode(const char *value) {+chroma_scale_mode_t parse_scale_mode(const char *value) {   if (!value)     return CHROMA_SCALE_FILL; // default @@ -103,7 +60,7 @@ static chroma_scale_mode_t parse_scale_mode(const char *value) { }  // Parse filter quality from string-static chroma_filter_quality_t parse_filter_quality(const char *value) {+chroma_filter_quality_t parse_filter_quality(const char *value) {   if (!value)     return CHROMA_FILTER_LINEAR; // default @@ -154,7 +111,7 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) { }  // Parse anchor position from string-static chroma_anchor_t parse_anchor(const char *value) {+chroma_anchor_t parse_anchor(const char *value) {   if (!value)     return CHROMA_ANCHOR_CENTER; @@ -213,12 +170,10 @@ static const char *anchor_to_string(chroma_anchor_t anchor) { }  // Output-to-image mapping-static int add_output_mapping(chroma_config_t *config, const char *output_name,-                              const char *image_path,-                              chroma_scale_mode_t scale_mode,-                              chroma_filter_quality_t filter_quality,-                              chroma_anchor_t anchor, float anchor_x,-                              float anchor_y) {+int add_output_mapping(chroma_config_t *config, const char *output_name,+                       const char *image_path, chroma_scale_mode_t scale_mode,+                       chroma_filter_quality_t filter_quality,+                       chroma_anchor_t anchor, float anchor_x, float anchor_y) {   if (!config || !output_name || !image_path) {     return CHROMA_ERROR_INIT;   }@@ -268,336 +223,221 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,   return CHROMA_OK; } -// Initialize configuration with defaults-static void init_default_config(chroma_config_t *config) {-  if (!config)-    return;--  memset(config, 0, sizeof(chroma_config_t));--  config->daemon_mode = false;-  config->mapping_count = 0;+// Parse TOML configuration file+int chroma_config_load_toml(chroma_config_t *config, const char *config_file) {+  if (!config || !config_file) {+    return CHROMA_ERROR_INIT;+  } -  // Set default scaling and filtering-  config->default_scale_mode = CHROMA_SCALE_FILL;-  config->default_filter_quality = CHROMA_FILTER_LINEAR;-  config->default_anchor = CHROMA_ANCHOR_CENTER;-  config->default_anchor_x = 50.0f; // center-  config->default_anchor_y = 50.0f; // center+  toml_result_t result = toml_parse_file_ex(config_file);+  if (!result.ok) {+    chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg);+    return CHROMA_ERROR_CONFIG;+  } -  // Set default downsampling settings-  config->enable_downsampling = true; // enable by default, performance etc.-  config->max_output_width = 3840;    // 4K width-  config->max_output_height = 2160;   // 4K height-  config->min_scale_factor = 0.25f;   // don't scale below 25%+  chroma_log("INFO", "Loading TOML configuration from: %s", config_file); -  // Set default image path (can be overridden)-  const char *home = getenv("HOME");-  if (home) {-    snprintf(config->default_image, sizeof(config->default_image),-             "%s/.config/chroma/default.jpg", home);-  } else {-    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");+  // Parse default_image+  toml_datum_t default_image = toml_seek(result.toptab, "default_image");+  if (default_image.type == TOML_STRING) {+    char *expanded_path = chroma_expand_path(default_image.u.s);+    const char *path_to_use = expanded_path ? expanded_path : default_image.u.s;+    if (strlen(path_to_use) < sizeof(config->default_image)) {+      strcpy(config->default_image, path_to_use);+    }+    if (expanded_path) {+      free(expanded_path);+    }   }-} -// Parse a single configuration line-static int parse_config_line(chroma_config_t *config, char *line,-                             int line_number) {-  if (!config || !line) {-    return CHROMA_ERROR_INIT;+  // Parse daemon_mode+  toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode");+  if (daemon_mode.type == TOML_BOOLEAN) {+    config->daemon_mode = daemon_mode.u.boolean;   } -  // Skip empty lines and comments-  char *trimmed = trim_whitespace(line);-  if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {-    return CHROMA_OK;+  // Parse scale_mode+  toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode");+  if (scale_mode.type == TOML_STRING) {+    config->default_scale_mode = parse_scale_mode(scale_mode.u.s);   } -  // Find the equals sign-  char *equals = strchr(trimmed, '=');-  if (!equals) {-    chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);-    return CHROMA_OK; // continue parsing+  // Parse filter_quality+  toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality");+  if (filter_quality.type == TOML_STRING) {+    config->default_filter_quality = parse_filter_quality(filter_quality.u.s);   } -  *equals = '\0';-  char *key = trim_whitespace(trimmed);-  char *value = trim_whitespace(equals + 1);+  // Parse anchor+  toml_datum_t anchor = toml_seek(result.toptab, "anchor");+  if (anchor.type == TOML_STRING) {+    config->default_anchor = parse_anchor(anchor.u.s);+  } -  value = remove_quotes(value);+  // Parse anchor_x+  toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x");+  if (anchor_x.type == TOML_INT64) {+    config->default_anchor_x = (float)anchor_x.u.int64;+  } else if (anchor_x.type == TOML_FP64) {+    config->default_anchor_x = (float)anchor_x.u.fp64;+  } -  if (*key == '\0' || *value == '\0') {-    chroma_log("WARN", "Invalid config line %d: empty key or value",-               line_number);-    return CHROMA_OK;+  // Parse anchor_y+  toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y");+  if (anchor_y.type == TOML_INT64) {+    config->default_anchor_y = (float)anchor_y.u.int64;+  } else if (anchor_y.type == TOML_FP64) {+    config->default_anchor_y = (float)anchor_y.u.fp64;   } -  // Parse configuration options-  if (strcasecmp(key, "default_image") == 0) {-    char *expanded_path = chroma_expand_path(value);-    const char *path_to_use = expanded_path ? expanded_path : value;-    size_t path_len = strlen(path_to_use);+  // Parse downsampling section+  toml_datum_t downsampling = toml_seek(result.toptab, "downsampling");+  if (downsampling.type == TOML_TABLE) {+    toml_datum_t ds_tab = downsampling; -    if (path_len >= sizeof(config->default_image)) {-      chroma_log("ERROR", "Default image path too long: %s (max %zu)",-                 path_to_use, sizeof(config->default_image) - 1);-      if (expanded_path) {-        free(expanded_path);-      }-      return CHROMA_ERROR_CONFIG;+    toml_datum_t enable = toml_seek(ds_tab, "enable");+    if (enable.type == TOML_BOOLEAN) {+      config->enable_downsampling = enable.u.boolean;     } -    strcpy(config->default_image, path_to_use);--    if (expanded_path) {-      chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);-      chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",-                 path_len, expanded_path);-      free(expanded_path);-    } else {-      chroma_log("WARN", "Failed to expand path, using original: %s", value);+    toml_datum_t max_width = toml_seek(ds_tab, "max_output_width");+    if (max_width.type == TOML_INT64) {+      config->max_output_width = (int)max_width.u.int64;     }-  } else if (strcasecmp(key, "daemon") == 0 ||-             strcasecmp(key, "daemon_mode") == 0) {-    config->daemon_mode = parse_bool(value);-    chroma_log("DEBUG", "Set daemon mode: %s",-               config->daemon_mode ? "true" : "false");-    chroma_log("TRACE",-               "Daemon mode configuration: key='%s', value='%s', parsed=%s",-               key, value, config->daemon_mode ? "true" : "false");-  } else if (strcasecmp(key, "scale_mode") == 0 ||-             strcasecmp(key, "default_scale_mode") == 0) {-    config->default_scale_mode = parse_scale_mode(value);-    chroma_log("DEBUG", "Set default scale mode: %s",-               scale_mode_to_string(config->default_scale_mode));-    chroma_log("TRACE",-               "Scale mode configuration: key='%s', value='%s', parsed=%s", key,-               value, scale_mode_to_string(config->default_scale_mode));-  } else if (strcasecmp(key, "filter_quality") == 0 ||-             strcasecmp(key, "default_filter_quality") == 0) {-    config->default_filter_quality = parse_filter_quality(value);-    chroma_log("DEBUG", "Set default filter quality: %s",-               filter_quality_to_string(config->default_filter_quality));-    chroma_log("TRACE",-               "Filter quality configuration: key='%s', value='%s', parsed=%s",-               key, value,-               filter_quality_to_string(config->default_filter_quality));-  } else if (strcasecmp(key, "enable_downsampling") == 0) {-    config->enable_downsampling = parse_bool(value);-    chroma_log("DEBUG", "Set downsampling: %s",-               config->enable_downsampling ? "enabled" : "disabled");-  } else if (strcasecmp(key, "anchor") == 0) {-    config->default_anchor = parse_anchor(value);-    chroma_log("DEBUG", "Set default anchor: %s",-               anchor_to_string(config->default_anchor));-  } else if (strcasecmp(key, "anchor_x") == 0) {-    char *endptr = NULL;-    float ax = strtof(value, &endptr);-    if (endptr == value || *endptr != '\0') {-      chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)",-                 value);-      config->default_anchor_x = 50.0f;-    } else if (ax < 0.0f || ax > 100.0f) {-      chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value);-      config->default_anchor_x = 50.0f;-    } else {-      config->default_anchor_x = ax;-      chroma_log("DEBUG", "Set default anchor_x: %.1f", (double)ax);-    }-  } else if (strcasecmp(key, "anchor_y") == 0) {-    char *endptr = NULL;-    float ay = strtof(value, &endptr);-    if (endptr == value || *endptr != '\0') {-      chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)",-                 value);-      config->default_anchor_y = 50.0f;-    } else if (ay < 0.0f || ay > 100.0f) {-      chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value);-      config->default_anchor_y = 50.0f;-    } else {-      config->default_anchor_y = ay;-      chroma_log("DEBUG", "Set default anchor_y: %.1f", (double)ay);-    }-  } else if (strcasecmp(key, "max_output_width") == 0) {-    int width = atoi(value);-    if (width > 0 && width <= 16384) { // Reasonable limits-      config->max_output_width = width;-      chroma_log("DEBUG", "Set max output width: %d", width);-    } else {-      chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);-    }-  } else if (strcasecmp(key, "max_output_height") == 0) {-    int height = atoi(value);-    if (height > 0 && height <= 16384) { // Reasonable limits-      config->max_output_height = height;-      chroma_log("DEBUG", "Set max output height: %d", height);-    } else {-      chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);-    }-  } else if (strcasecmp(key, "min_scale_factor") == 0) {-    float factor = (float)atof(value);-    if (factor > 0.0f && factor <= 1.0f) {-      config->min_scale_factor = factor;-      chroma_log("DEBUG", "Set minimum scale factor: %.2f", (double)factor);-    } else {-      chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);++    toml_datum_t max_height = toml_seek(ds_tab, "max_output_height");+    if (max_height.type == TOML_INT64) {+      config->max_output_height = (int)max_height.u.int64;     }-  } else if (strncasecmp(key, "output.", 7) == 0) {-    // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg-    const char *output_name = key + 7;-    if (*output_name == '\0') {-      chroma_log("WARN", "Invalid output mapping line %d: no output name",-                 line_number);-      return CHROMA_OK;++    toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor");+    if (min_scale.type == TOML_FP64) {+      config->min_scale_factor = (float)min_scale.u.fp64;+    } else if (min_scale.type == TOML_INT64) {+      config->min_scale_factor = (float)min_scale.u.int64;     }+  } -    // Check for extended output configuration with properties. Format:-    // output.DP-1.scale = fill-    // output.DP-1.filter = linear-    char *dot = strchr(output_name, '.');-    if (dot) {-      // This is an output property (scale or filter)-      *dot = '\0';-      const char *property = dot + 1;--      // Find existing mapping for this output-      chroma_config_mapping_t *mapping = NULL;-      for (int i = 0; i < config->mapping_count; i++) {-        if (strcmp(config->mappings[i].output_name, output_name) == 0) {-          mapping = &config->mappings[i];-          break;-        }+  // Parse output mappings array+  toml_datum_t outputs = toml_seek(result.toptab, "output");+  if (outputs.type == TOML_ARRAY) {+    for (int i = 0; i < outputs.u.arr.size; i++) {+      toml_datum_t output = outputs.u.arr.elem[i];+      if (output.type != TOML_TABLE) {+        continue;       } -      if (!mapping) {-        chroma_log("WARN", "Output %s not found for property %s (line %d)",-                   output_name, property, line_number);-        return CHROMA_OK;+      toml_datum_t output_tab = output;++      // Get output name+      toml_datum_t name = toml_seek(output_tab, "name");+      if (name.type != TOML_STRING) {+        chroma_log("WARN", "Output mapping %d missing name", i);+        continue;       } -      if (strcasecmp(property, "scale") == 0) {-        mapping->scale_mode = parse_scale_mode(value);-        chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,-                   scale_mode_to_string(mapping->scale_mode));-      } else if (strcasecmp(property, "filter") == 0) {-        mapping->filter_quality = parse_filter_quality(value);-        chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,-                   filter_quality_to_string(mapping->filter_quality));-      } else if (strcasecmp(property, "anchor") == 0) {-        mapping->anchor = parse_anchor(value);-        // Set anchor_x/anchor_y based on named anchor-        switch (mapping->anchor) {-        case CHROMA_ANCHOR_TOP:-          mapping->anchor_x = 50.0f;-          mapping->anchor_y = 0.0f;-          break;-        case CHROMA_ANCHOR_BOTTOM:-          mapping->anchor_x = 50.0f;-          mapping->anchor_y = 100.0f;-          break;-        case CHROMA_ANCHOR_LEFT:-          mapping->anchor_x = 0.0f;-          mapping->anchor_y = 50.0f;-          break;-        case CHROMA_ANCHOR_RIGHT:-          mapping->anchor_x = 100.0f;-          mapping->anchor_y = 50.0f;-          break;-        case CHROMA_ANCHOR_TOP_LEFT:-          mapping->anchor_x = 0.0f;-          mapping->anchor_y = 0.0f;-          break;-        case CHROMA_ANCHOR_TOP_RIGHT:-          mapping->anchor_x = 100.0f;-          mapping->anchor_y = 0.0f;-          break;-        case CHROMA_ANCHOR_BOTTOM_LEFT:-          mapping->anchor_x = 0.0f;-          mapping->anchor_y = 100.0f;-          break;-        case CHROMA_ANCHOR_BOTTOM_RIGHT:-          mapping->anchor_x = 100.0f;-          mapping->anchor_y = 100.0f;-          break;-        default:-          mapping->anchor_x = 50.0f;-          mapping->anchor_y = 50.0f;-          break;-        }-        chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",-                   output_name, anchor_to_string(mapping->anchor),-                   (double)mapping->anchor_x, (double)mapping->anchor_y);-      } else if (strcasecmp(property, "anchor_x") == 0) {-        float ax = (float)atof(value);-        if (ax >= 0.0f && ax <= 100.0f) {-          mapping->anchor_x = ax;-          chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name,-                     (double)ax);-        } else {-          mapping->anchor_x = 50.0f;-          chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)",-                     value);-        }-      } else if (strcasecmp(property, "anchor_y") == 0) {-        float ay = (float)atof(value);-        if (ay >= 0.0f && ay <= 100.0f) {-          mapping->anchor_y = ay;-          chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name,-                     (double)ay);-        } else {-          mapping->anchor_y = 50.0f;-          chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",-                     value);-        }-      } else {-        chroma_log("WARN", "Unknown output property: %s (line %d)", property,-                   line_number);+      // Get image path+      toml_datum_t image = toml_seek(output_tab, "image");+      if (image.type != TOML_STRING) {+        chroma_log("WARN", "Output mapping %d missing image", i);+        continue;       } -      return CHROMA_OK;-    }+      char *expanded_path = chroma_expand_path(image.u.s);+      const char *path_to_use = expanded_path ? expanded_path : image.u.s; -    // Expand path before validation and storage-    char *expanded_path = chroma_expand_path(value);-    const char *path_to_validate = expanded_path ? expanded_path : value;+      // Get optional settings with defaults from global config+      chroma_scale_mode_t out_scale = config->default_scale_mode;+      chroma_filter_quality_t out_filter = config->default_filter_quality;+      chroma_anchor_t out_anchor = config->default_anchor;+      float out_anchor_x = config->default_anchor_x;+      float out_anchor_y = config->default_anchor_y; -    // Validate image path-    if (chroma_image_validate(path_to_validate) != CHROMA_OK) {-      chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",-                 output_name, value, path_to_validate);-      if (expanded_path) {-        free(expanded_path);+      toml_datum_t out_scale_val = toml_seek(output_tab, "scale");+      if (out_scale_val.type == TOML_STRING) {+        out_scale = parse_scale_mode(out_scale_val.u.s);+      }++      toml_datum_t out_filter_val = toml_seek(output_tab, "filter");+      if (out_filter_val.type == TOML_STRING) {+        out_filter = parse_filter_quality(out_filter_val.u.s);+      }++      toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor");+      if (out_anchor_val.type == TOML_STRING) {+        out_anchor = parse_anchor(out_anchor_val.u.s);+      }++      toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x");+      if (out_anchor_x_val.type == TOML_INT64) {+        out_anchor_x = (float)out_anchor_x_val.u.int64;+      } else if (out_anchor_x_val.type == TOML_FP64) {+        out_anchor_x = (float)out_anchor_x_val.u.fp64;+      }++      toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y");+      if (out_anchor_y_val.type == TOML_INT64) {+        out_anchor_y = (float)out_anchor_y_val.u.int64;+      } else if (out_anchor_y_val.type == TOML_FP64) {+        out_anchor_y = (float)out_anchor_y_val.u.fp64;+      }++      // Add the mapping+      if (add_output_mapping(config, name.u.s, path_to_use, out_scale,+                             out_filter, out_anchor, out_anchor_x,+                             out_anchor_y) != CHROMA_OK) {+        chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s);       }-      return CHROMA_OK;-    } -    if (add_output_mapping(-            config, output_name, path_to_validate, config->default_scale_mode,-            config->default_filter_quality, config->default_anchor,-            config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) {-      chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,-                 path_to_validate);       if (expanded_path) {         free(expanded_path);       }-      return CHROMA_ERROR_CONFIG;     }--    if (expanded_path) {-      free(expanded_path);-    }-  } else {-    chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,-               key);-    chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",-               line_number, key, value);   } +  toml_free(result);++  chroma_log("INFO",+             "Loaded TOML configuration: %d output mappings, default image: %s",+             config->mapping_count, config->default_image);+   return CHROMA_OK; } -// Load configuration from file+// Initialize configuration with defaults+static void init_default_config(chroma_config_t *config) {+  if (!config)+    return;++  memset(config, 0, sizeof(chroma_config_t));++  config->daemon_mode = false;+  config->mapping_count = 0;++  // Set default scaling and filtering+  config->default_scale_mode = CHROMA_SCALE_FILL;+  config->default_filter_quality = CHROMA_FILTER_LINEAR;+  config->default_anchor = CHROMA_ANCHOR_CENTER;+  config->default_anchor_x = 50.0f; // center+  config->default_anchor_y = 50.0f; // center++  // Set default downsampling settings+  config->enable_downsampling = true; // enable by default, performance etc.+  config->max_output_width = 3840;    // 4K width+  config->max_output_height = 2160;   // 4K height+  config->min_scale_factor = 0.25f;   // don't scale below 25%++  // Set default image path (can be overridden)+  const char *home = getenv("HOME");+  if (home) {+    snprintf(config->default_image, sizeof(config->default_image),+             "%s/.config/chroma/default.jpg", home);+  } else {+    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");+  }+}++// Load configuration from file (TOML format only) int chroma_config_load(chroma_config_t *config, const char *config_file) {   if (!config) {     return CHROMA_ERROR_INIT;@@ -611,52 +451,20 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) {     return CHROMA_OK;   } -  FILE *file = fopen(config_file, "r");-  if (!file) {-    if (errno == ENOENT) {-      chroma_log("INFO", "Config file not found: %s (using defaults)",-                 config_file);-      return CHROMA_OK;-    } else {-      chroma_log("ERROR", "Failed to open config file %s: %s", config_file,-                 strerror(errno));-      return CHROMA_ERROR_CONFIG;-    }-  }--  chroma_log("INFO", "Loading configuration from: %s", config_file);-  chroma_log("TRACE",-             "Starting configuration parsing, estimated config size: %ld bytes",-             chroma_get_file_size(config_file));--  char line[1024];-  int line_number = 0;-  int parse_errors = 0;--  while (fgets(line, sizeof(line), file)) {-    line_number++;--    char *newline = strchr(line, '\n');-    if (newline) {-      *newline = '\0';-    }--    if (parse_config_line(config, line, line_number) != CHROMA_OK) {-      parse_errors++;-    }+  // Check if file exists+  if (!chroma_path_exists(config_file)) {+    chroma_log("INFO", "Config file not found: %s (using defaults)",+               config_file);+    return CHROMA_OK;   } -  fclose(file);--  if (parse_errors > 0) {-    chroma_log("WARN", "Config file contained %d errors", parse_errors);-    // Continue anyway with partial configuration+  // Load TOML configuration+  int result = chroma_config_load_toml(config, config_file);+  if (result != CHROMA_OK) {+    chroma_log("ERROR", "Failed to load TOML config from %s", config_file);+    return result;   } -  chroma_log("INFO",-             "Loaded configuration: %d output mappings, default image: %s",-             config->mapping_count, config->default_image);-   // Log configuration memory usage   size_t config_size =       sizeof(chroma_config_t) +