brewery
notashelf /
e1f534d0e6c317bfae0dfd4c6b00703bdcfc136b

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/config.cC618 lines20.7 KB
1
#include <ctype.h>
2
#include <errno.h>
3
#include <stdio.h>
4
#include <stdlib.h>
5
#include <string.h>
6
#include <strings.h>
7
8
#include "chroma.h"
9
#include "vendor/tomlc17.h"
10
11
// Match output name/description against a config pattern
12
// Supports:
13
//   - Exact name match: "DP-1" matches wl_output.name == "DP-1"
14
//   - Description prefix match: "desc:Samsung" matches if description starts
15
//   with "Samsung"
16
static bool match_output(const char *pattern, const char *output_name,
17
                         const char *output_description) {
18
  if (!pattern || !output_name) {
19
    return false;
20
  }
21
22
  // Try exact name match first
23
  if (strcmp(pattern, output_name) == 0) {
24
    return true;
25
  }
26
27
  // Check for description prefix match: "desc:<prefix>"
28
  if (strncmp(pattern, "desc:", 5) == 0) {
29
    const char *desc_prefix = pattern + 5;
30
    size_t prefix_len = strlen(desc_prefix);
31
32
    if (output_description && prefix_len > 0) {
33
      // Match if description starts with the prefix (case-insensitive)
34
      if (strncasecmp(output_description, desc_prefix, prefix_len) == 0) {
35
        return true;
36
      }
37
    }
38
  }
39
40
  return false;
41
}
42
43
// Parse scaling mode from string
44
chroma_scale_mode_t parse_scale_mode(const char *value) {
45
  if (!value)
46
    return CHROMA_SCALE_FILL; // default
47
48
  if (strcasecmp(value, "fill") == 0) {
49
    return CHROMA_SCALE_FILL;
50
  } else if (strcasecmp(value, "fit") == 0) {
51
    return CHROMA_SCALE_FIT;
52
  } else if (strcasecmp(value, "stretch") == 0) {
53
    return CHROMA_SCALE_STRETCH;
54
  } else if (strcasecmp(value, "center") == 0) {
55
    return CHROMA_SCALE_CENTER;
56
  }
57
58
  chroma_log("WARN", "Unknown scaling mode: %s (using fill)", value);
59
  return CHROMA_SCALE_FILL;
60
}
61
62
// Parse filter quality from string
63
chroma_filter_quality_t parse_filter_quality(const char *value) {
64
  if (!value)
65
    return CHROMA_FILTER_LINEAR; // default
66
67
  if (strcasecmp(value, "nearest") == 0) {
68
    return CHROMA_FILTER_NEAREST;
69
  } else if (strcasecmp(value, "linear") == 0) {
70
    return CHROMA_FILTER_LINEAR;
71
  } else if (strcasecmp(value, "bilinear") == 0) {
72
    return CHROMA_FILTER_BILINEAR;
73
  } else if (strcasecmp(value, "trilinear") == 0) {
74
    return CHROMA_FILTER_TRILINEAR;
75
  }
76
77
  chroma_log("WARN", "Unknown filter quality: %s (using linear)", value);
78
  return CHROMA_FILTER_LINEAR;
79
}
80
81
// Get string representation of scaling mode
82
static const char *scale_mode_to_string(chroma_scale_mode_t mode) {
83
  switch (mode) {
84
  case CHROMA_SCALE_FILL:
85
    return "fill";
86
  case CHROMA_SCALE_FIT:
87
    return "fit";
88
  case CHROMA_SCALE_STRETCH:
89
    return "stretch";
90
  case CHROMA_SCALE_CENTER:
91
    return "center";
92
  default:
93
    return "unknown";
94
  }
95
}
96
97
// Get string representation of filter quality
98
static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
99
  switch (quality) {
100
  case CHROMA_FILTER_NEAREST:
101
    return "nearest";
102
  case CHROMA_FILTER_LINEAR:
103
    return "linear";
104
  case CHROMA_FILTER_BILINEAR:
105
    return "bilinear";
106
  case CHROMA_FILTER_TRILINEAR:
107
    return "trilinear";
108
  default:
109
    return "unknown";
110
  }
111
}
112
113
// Parse anchor position from string
114
chroma_anchor_t parse_anchor(const char *value) {
115
  if (!value)
116
    return CHROMA_ANCHOR_CENTER;
117
118
  if (strcasecmp(value, "center") == 0) {
119
    return CHROMA_ANCHOR_CENTER;
120
  } else if (strcasecmp(value, "top") == 0) {
121
    return CHROMA_ANCHOR_TOP;
122
  } else if (strcasecmp(value, "bottom") == 0) {
123
    return CHROMA_ANCHOR_BOTTOM;
124
  } else if (strcasecmp(value, "left") == 0) {
125
    return CHROMA_ANCHOR_LEFT;
126
  } else if (strcasecmp(value, "right") == 0) {
127
    return CHROMA_ANCHOR_RIGHT;
128
  } else if (strcasecmp(value, "top-left") == 0 ||
129
             strcasecmp(value, "topleft") == 0) {
130
    return CHROMA_ANCHOR_TOP_LEFT;
131
  } else if (strcasecmp(value, "top-right") == 0 ||
132
             strcasecmp(value, "topright") == 0) {
133
    return CHROMA_ANCHOR_TOP_RIGHT;
134
  } else if (strcasecmp(value, "bottom-left") == 0 ||
135
             strcasecmp(value, "bottomleft") == 0) {
136
    return CHROMA_ANCHOR_BOTTOM_LEFT;
137
  } else if (strcasecmp(value, "bottom-right") == 0 ||
138
             strcasecmp(value, "bottomright") == 0) {
139
    return CHROMA_ANCHOR_BOTTOM_RIGHT;
140
  }
141
142
  chroma_log("WARN", "Unknown anchor: %s (using center)", value);
143
  return CHROMA_ANCHOR_CENTER;
144
}
145
146
// Get string representation of anchor position
147
static const char *anchor_to_string(chroma_anchor_t anchor) {
148
  switch (anchor) {
149
  case CHROMA_ANCHOR_CENTER:
150
    return "center";
151
  case CHROMA_ANCHOR_TOP:
152
    return "top";
153
  case CHROMA_ANCHOR_BOTTOM:
154
    return "bottom";
155
  case CHROMA_ANCHOR_LEFT:
156
    return "left";
157
  case CHROMA_ANCHOR_RIGHT:
158
    return "right";
159
  case CHROMA_ANCHOR_TOP_LEFT:
160
    return "top-left";
161
  case CHROMA_ANCHOR_TOP_RIGHT:
162
    return "top-right";
163
  case CHROMA_ANCHOR_BOTTOM_LEFT:
164
    return "bottom-left";
165
  case CHROMA_ANCHOR_BOTTOM_RIGHT:
166
    return "bottom-right";
167
  default:
168
    return "unknown";
169
  }
170
}
171
172
// Output-to-image mapping
173
int add_output_mapping(chroma_config_t *config, const char *output_name,
174
                       const char *image_path, chroma_scale_mode_t scale_mode,
175
                       chroma_filter_quality_t filter_quality,
176
                       chroma_anchor_t anchor, float anchor_x, float anchor_y) {
177
  if (!config || !output_name || !image_path) {
178
    return CHROMA_ERROR_INIT;
179
  }
180
181
  if (config->mapping_count >= MAX_OUTPUTS) {
182
    chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
183
               MAX_OUTPUTS);
184
    return CHROMA_ERROR_MEMORY;
185
  }
186
187
  // XXX: Validate string lengths to prevent buffer overflow
188
  size_t output_len = strlen(output_name);
189
  size_t path_len = strlen(image_path);
190
191
  if (output_len >= sizeof(config->mappings[0].output_name)) {
192
    chroma_log("ERROR", "Output name too long: %s (max %zu)", output_name,
193
               sizeof(config->mappings[0].output_name) - 1);
194
    return CHROMA_ERROR_CONFIG;
195
  }
196
197
  if (path_len >= sizeof(config->mappings[0].image_path)) {
198
    chroma_log("ERROR", "Image path too long: %s (max %zu)", image_path,
199
               sizeof(config->mappings[0].image_path) - 1);
200
    return CHROMA_ERROR_CONFIG;
201
  }
202
203
  chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
204
205
  strcpy(mapping->output_name, output_name);
206
  strcpy(mapping->image_path, image_path);
207
  mapping->scale_mode = scale_mode;
208
  mapping->filter_quality = filter_quality;
209
  mapping->anchor = anchor;
210
  mapping->anchor_x = anchor_x;
211
  mapping->anchor_y = anchor_y;
212
213
  config->mapping_count++;
214
215
  chroma_log(
216
      "DEBUG",
217
      "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s @ %.1f,%.1f)",
218
      output_name, image_path, scale_mode_to_string(scale_mode),
219
      filter_quality_to_string(filter_quality), anchor_to_string(anchor),
220
      (double)anchor_x, (double)anchor_y);
221
  chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
222
             config->mapping_count, output_name, image_path, path_len);
223
  return CHROMA_OK;
224
}
225
226
// Parse TOML configuration file
227
int chroma_config_load_toml(chroma_config_t *config, const char *config_file) {
228
  if (!config || !config_file) {
229
    return CHROMA_ERROR_INIT;
230
  }
231
232
  toml_result_t result = toml_parse_file_ex(config_file);
233
  if (!result.ok) {
234
    chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg);
235
    return CHROMA_ERROR_CONFIG;
236
  }
237
238
  chroma_log("INFO", "Loading TOML configuration from: %s", config_file);
239
240
  // Parse default_image
241
  toml_datum_t default_image = toml_seek(result.toptab, "default_image");
242
  if (default_image.type == TOML_STRING) {
243
    char *expanded_path = chroma_expand_path(default_image.u.s);
244
    const char *path_to_use = expanded_path ? expanded_path : default_image.u.s;
245
    if (strlen(path_to_use) < sizeof(config->default_image)) {
246
      strcpy(config->default_image, path_to_use);
247
    }
248
    if (expanded_path) {
249
      free(expanded_path);
250
    }
251
  }
252
253
  // Parse daemon_mode
254
  toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode");
255
  if (daemon_mode.type == TOML_BOOLEAN) {
256
    config->daemon_mode = daemon_mode.u.boolean;
257
  }
258
259
  // Parse scale_mode
260
  toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode");
261
  if (scale_mode.type == TOML_STRING) {
262
    config->default_scale_mode = parse_scale_mode(scale_mode.u.s);
263
  }
264
265
  // Parse filter_quality
266
  toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality");
267
  if (filter_quality.type == TOML_STRING) {
268
    config->default_filter_quality = parse_filter_quality(filter_quality.u.s);
269
  }
270
271
  // Parse anchor
272
  toml_datum_t anchor = toml_seek(result.toptab, "anchor");
273
  if (anchor.type == TOML_STRING) {
274
    config->default_anchor = parse_anchor(anchor.u.s);
275
  }
276
277
  // Parse anchor_x
278
  toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x");
279
  if (anchor_x.type == TOML_INT64) {
280
    config->default_anchor_x = (float)anchor_x.u.int64;
281
  } else if (anchor_x.type == TOML_FP64) {
282
    config->default_anchor_x = (float)anchor_x.u.fp64;
283
  }
284
285
  // Parse anchor_y
286
  toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y");
287
  if (anchor_y.type == TOML_INT64) {
288
    config->default_anchor_y = (float)anchor_y.u.int64;
289
  } else if (anchor_y.type == TOML_FP64) {
290
    config->default_anchor_y = (float)anchor_y.u.fp64;
291
  }
292
293
  // Parse downsampling section
294
  toml_datum_t downsampling = toml_seek(result.toptab, "downsampling");
295
  if (downsampling.type == TOML_TABLE) {
296
    toml_datum_t ds_tab = downsampling;
297
298
    toml_datum_t enable = toml_seek(ds_tab, "enable");
299
    if (enable.type == TOML_BOOLEAN) {
300
      config->enable_downsampling = enable.u.boolean;
301
    }
302
303
    toml_datum_t max_width = toml_seek(ds_tab, "max_output_width");
304
    if (max_width.type == TOML_INT64) {
305
      config->max_output_width = (int)max_width.u.int64;
306
    }
307
308
    toml_datum_t max_height = toml_seek(ds_tab, "max_output_height");
309
    if (max_height.type == TOML_INT64) {
310
      config->max_output_height = (int)max_height.u.int64;
311
    }
312
313
    toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor");
314
    if (min_scale.type == TOML_FP64) {
315
      config->min_scale_factor = (float)min_scale.u.fp64;
316
    } else if (min_scale.type == TOML_INT64) {
317
      config->min_scale_factor = (float)min_scale.u.int64;
318
    }
319
  }
320
321
  // Parse output mappings array
322
  toml_datum_t outputs = toml_seek(result.toptab, "output");
323
  if (outputs.type == TOML_ARRAY) {
324
    for (int i = 0; i < outputs.u.arr.size; i++) {
325
      toml_datum_t output = outputs.u.arr.elem[i];
326
      if (output.type != TOML_TABLE) {
327
        continue;
328
      }
329
330
      toml_datum_t output_tab = output;
331
332
      // Get output name
333
      toml_datum_t name = toml_seek(output_tab, "name");
334
      if (name.type != TOML_STRING) {
335
        chroma_log("WARN", "Output mapping %d missing name", i);
336
        continue;
337
      }
338
339
      // Get image path
340
      toml_datum_t image = toml_seek(output_tab, "image");
341
      if (image.type != TOML_STRING) {
342
        chroma_log("WARN", "Output mapping %d missing image", i);
343
        continue;
344
      }
345
346
      char *expanded_path = chroma_expand_path(image.u.s);
347
      const char *path_to_use = expanded_path ? expanded_path : image.u.s;
348
349
      // Get optional settings with defaults from global config
350
      chroma_scale_mode_t out_scale = config->default_scale_mode;
351
      chroma_filter_quality_t out_filter = config->default_filter_quality;
352
      chroma_anchor_t out_anchor = config->default_anchor;
353
      float out_anchor_x = config->default_anchor_x;
354
      float out_anchor_y = config->default_anchor_y;
355
356
      toml_datum_t out_scale_val = toml_seek(output_tab, "scale");
357
      if (out_scale_val.type == TOML_STRING) {
358
        out_scale = parse_scale_mode(out_scale_val.u.s);
359
      }
360
361
      toml_datum_t out_filter_val = toml_seek(output_tab, "filter");
362
      if (out_filter_val.type == TOML_STRING) {
363
        out_filter = parse_filter_quality(out_filter_val.u.s);
364
      }
365
366
      toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor");
367
      if (out_anchor_val.type == TOML_STRING) {
368
        out_anchor = parse_anchor(out_anchor_val.u.s);
369
      }
370
371
      toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x");
372
      if (out_anchor_x_val.type == TOML_INT64) {
373
        out_anchor_x = (float)out_anchor_x_val.u.int64;
374
      } else if (out_anchor_x_val.type == TOML_FP64) {
375
        out_anchor_x = (float)out_anchor_x_val.u.fp64;
376
      }
377
378
      toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y");
379
      if (out_anchor_y_val.type == TOML_INT64) {
380
        out_anchor_y = (float)out_anchor_y_val.u.int64;
381
      } else if (out_anchor_y_val.type == TOML_FP64) {
382
        out_anchor_y = (float)out_anchor_y_val.u.fp64;
383
      }
384
385
      // Add the mapping
386
      if (add_output_mapping(config, name.u.s, path_to_use, out_scale,
387
                             out_filter, out_anchor, out_anchor_x,
388
                             out_anchor_y) != CHROMA_OK) {
389
        chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s);
390
      }
391
392
      if (expanded_path) {
393
        free(expanded_path);
394
      }
395
    }
396
  }
397
398
  toml_free(result);
399
400
  chroma_log("INFO",
401
             "Loaded TOML configuration: %d output mappings, default image: %s",
402
             config->mapping_count, config->default_image);
403
404
  return CHROMA_OK;
405
}
406
407
// Initialize configuration with defaults
408
static void init_default_config(chroma_config_t *config) {
409
  if (!config)
410
    return;
411
412
  memset(config, 0, sizeof(chroma_config_t));
413
414
  config->daemon_mode = false;
415
  config->mapping_count = 0;
416
417
  // Set default scaling and filtering
418
  config->default_scale_mode = CHROMA_SCALE_FILL;
419
  config->default_filter_quality = CHROMA_FILTER_LINEAR;
420
  config->default_anchor = CHROMA_ANCHOR_CENTER;
421
  config->default_anchor_x = 50.0f; // center
422
  config->default_anchor_y = 50.0f; // center
423
424
  // Set default downsampling settings
425
  config->enable_downsampling = true; // enable by default, performance etc.
426
  config->max_output_width = 3840;    // 4K width
427
  config->max_output_height = 2160;   // 4K height
428
  config->min_scale_factor = 0.25f;   // don't scale below 25%
429
430
  // Set default image path (can be overridden)
431
  const char *home = getenv("HOME");
432
  if (home) {
433
    snprintf(config->default_image, sizeof(config->default_image),
434
             "%s/.config/chroma/default.jpg", home);
435
  } else {
436
    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
437
  }
438
}
439
440
// Load configuration from file (TOML format only)
441
int chroma_config_load(chroma_config_t *config, const char *config_file) {
442
  if (!config) {
443
    return CHROMA_ERROR_INIT;
444
  }
445
446
  // Initialize with defaults
447
  init_default_config(config);
448
449
  if (!config_file) {
450
    chroma_log("INFO", "No config file specified, using defaults");
451
    return CHROMA_OK;
452
  }
453
454
  // Check if file exists
455
  if (!chroma_path_exists(config_file)) {
456
    chroma_log("INFO", "Config file not found: %s (using defaults)",
457
               config_file);
458
    return CHROMA_OK;
459
  }
460
461
  // Load TOML configuration
462
  int result = chroma_config_load_toml(config, config_file);
463
  if (result != CHROMA_OK) {
464
    chroma_log("ERROR", "Failed to load TOML config from %s", config_file);
465
    return result;
466
  }
467
468
  // Log configuration memory usage
469
  size_t config_size =
470
      sizeof(chroma_config_t) +
471
      ((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
472
  chroma_log_resource_allocation("config_data", config_size,
473
                                 "configuration structure");
474
  chroma_log_memory_stats("post-config-load");
475
476
  return CHROMA_OK;
477
}
478
479
// Free configuration resources
480
void chroma_config_free(chroma_config_t *config) {
481
  if (!config) {
482
    return;
483
  }
484
485
  // Log configuration deallocation
486
  size_t config_size =
487
      sizeof(chroma_config_t) +
488
      ((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
489
  chroma_log_resource_deallocation("config_data", config_size,
490
                                   "configuration structure");
491
492
  chroma_log("TRACE", "Clearing %d output mappings from configuration",
493
             config->mapping_count);
494
  memset(config->mappings, 0, sizeof(config->mappings));
495
  config->mapping_count = 0;
496
497
  memset(config->default_image, 0, sizeof(config->default_image));
498
499
  chroma_log("DEBUG", "Freed configuration");
500
}
501
502
// Get image path for specific output
503
const char *chroma_config_get_image_for_output(chroma_config_t *config,
504
                                               const char *output_name,
505
                                               const char *output_description) {
506
  if (!config || !output_name) {
507
    return NULL;
508
  }
509
510
  // Look for specific output mapping (name or description match)
511
  for (int i = 0; i < config->mapping_count; i++) {
512
    if (match_output(config->mappings[i].output_name, output_name,
513
                     output_description)) {
514
      chroma_log("DEBUG", "Found specific mapping for output %s (desc: %s): %s",
515
                 output_name, output_description ? output_description : "none",
516
                 config->mappings[i].image_path);
517
      return config->mappings[i].image_path;
518
    }
519
  }
520
521
  // Return default image if no specific mapping found
522
  if (strlen(config->default_image) > 0) {
523
    chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
524
               config->default_image);
525
    return config->default_image;
526
  }
527
528
  chroma_log("WARN", "No image configured for output: %s", output_name);
529
  return NULL;
530
}
531
532
// Get configuration mapping for output, including scale mode, filter
533
// quality, anchor, and custom anchor coordinates
534
int chroma_config_get_mapping_for_output(
535
    chroma_config_t *config, const char *output_name,
536
    const char *output_description, chroma_scale_mode_t *scale_mode,
537
    chroma_filter_quality_t *filter_quality, chroma_anchor_t *anchor,
538
    float *anchor_x, float *anchor_y) {
539
  if (!config || !output_name || !scale_mode || !filter_quality || !anchor ||
540
      !anchor_x || !anchor_y) {
541
    return CHROMA_ERROR_INIT;
542
  }
543
544
  // Look for specific output mapping (name or description match)
545
  for (int i = 0; i < config->mapping_count; i++) {
546
    if (match_output(config->mappings[i].output_name, output_name,
547
                     output_description)) {
548
      *scale_mode = config->mappings[i].scale_mode;
549
      *filter_quality = config->mappings[i].filter_quality;
550
      *anchor = config->mappings[i].anchor;
551
      *anchor_x = config->mappings[i].anchor_x;
552
      *anchor_y = config->mappings[i].anchor_y;
553
      chroma_log("DEBUG",
554
                 "Found specific mapping for output %s (desc: %s): scale=%s, "
555
                 "filter=%s, anchor=%s @ %.1f,%.1f",
556
                 output_name, output_description ? output_description : "none",
557
                 scale_mode_to_string(*scale_mode),
558
                 filter_quality_to_string(*filter_quality),
559
                 anchor_to_string(*anchor), (double)*anchor_x,
560
                 (double)*anchor_y);
561
      return CHROMA_OK;
562
    }
563
  }
564
565
  // Return defaults if no specific mapping found
566
  *scale_mode = config->default_scale_mode;
567
  *filter_quality = config->default_filter_quality;
568
  *anchor = config->default_anchor;
569
  *anchor_x = config->default_anchor_x;
570
  *anchor_y = config->default_anchor_y;
571
  chroma_log("DEBUG",
572
             "Using defaults for output %s: scale=%s, filter=%s, anchor=%s @ "
573
             "%.1f,%.1f",
574
             output_name, scale_mode_to_string(*scale_mode),
575
             filter_quality_to_string(*filter_quality),
576
             anchor_to_string(*anchor), (double)*anchor_x, (double)*anchor_y);
577
  return CHROMA_OK;
578
}
579
580
// Print current configuration for debugging
581
void chroma_config_print(const chroma_config_t *config) {
582
  if (!config) {
583
    return;
584
  }
585
586
  chroma_log("INFO", "=== Configuration ===");
587
  chroma_log("INFO", "Default image: %s", config->default_image);
588
  chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
589
  chroma_log("INFO", "Default scale mode: %s",
590
             scale_mode_to_string(config->default_scale_mode));
591
  chroma_log("INFO", "Default filter quality: %s",
592
             filter_quality_to_string(config->default_filter_quality));
593
  chroma_log("INFO", "Downsampling: %s",
594
             config->enable_downsampling ? "enabled" : "disabled");
595
  if (config->enable_downsampling) {
596
    chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
597
               config->max_output_height);
598
    chroma_log("INFO", "Min scale factor: %.2f",
599
               (double)config->min_scale_factor);
600
  }
601
  chroma_log("INFO", "Output mappings: %d", config->mapping_count);
602
603
  for (int i = 0; i < config->mapping_count; i++) {
604
    chroma_log("INFO", "  %s -> %s (scale: %s, filter: %s)",
605
               config->mappings[i].output_name, config->mappings[i].image_path,
606
               scale_mode_to_string(config->mappings[i].scale_mode),
607
               filter_quality_to_string(config->mappings[i].filter_quality));
608
    chroma_log(
609
        "TRACE",
610
        "  Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
611
        "path_exists=%s",
612
        i, config->mappings[i].output_name, config->mappings[i].image_path,
613
        scale_mode_to_string(config->mappings[i].scale_mode),
614
        filter_quality_to_string(config->mappings[i].filter_quality),
615
        chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
616
  }
617
  chroma_log("INFO", "====================");
618
}