brewery
notashelf /
5df01492ec6fbbbabb58816ea19e12f00905b888

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/config.cC769 lines26.1 KB
1
#include <ctype.h>
2
#include <errno.h>
3
#include <stdio.h>
4
#include <stdlib.h>
5
#include <string.h>
6
7
#include "../include/chroma.h"
8
9
static char *trim_whitespace(char *str) {
10
  char *end;
11
12
  // Trim leading whitespace
13
  while (isspace((unsigned char)*str))
14
    str++;
15
16
  // All spaces?
17
  if (*str == 0)
18
    return str;
19
20
  // Trim trailing whitespace
21
  end = str + strlen(str) - 1;
22
  while (end > str && isspace((unsigned char)*end))
23
    end--;
24
25
  *(end + 1) = '\0';
26
  return str;
27
}
28
29
// Remove quotes from a string
30
static char *remove_quotes(char *str) {
31
  size_t len = strlen(str);
32
  if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
33
                   (str[0] == '\'' && str[len - 1] == '\''))) {
34
    str[len - 1] = '\0';
35
    return str + 1;
36
  }
37
  return str;
38
}
39
40
// Parse boolean value from string
41
static bool parse_bool(const char *value) {
42
  if (!value)
43
    return false;
44
45
  if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||
46
      strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {
47
    return true;
48
  }
49
50
  return false;
51
}
52
53
// Parse scaling mode from string
54
static chroma_scale_mode_t parse_scale_mode(const char *value) {
55
  if (!value)
56
    return CHROMA_SCALE_FILL; // default
57
58
  if (strcasecmp(value, "fill") == 0) {
59
    return CHROMA_SCALE_FILL;
60
  } else if (strcasecmp(value, "fit") == 0) {
61
    return CHROMA_SCALE_FIT;
62
  } else if (strcasecmp(value, "stretch") == 0) {
63
    return CHROMA_SCALE_STRETCH;
64
  } else if (strcasecmp(value, "center") == 0) {
65
    return CHROMA_SCALE_CENTER;
66
  }
67
68
  chroma_log("WARN", "Unknown scaling mode: %s (using fill)", value);
69
  return CHROMA_SCALE_FILL;
70
}
71
72
// Parse filter quality from string
73
static chroma_filter_quality_t parse_filter_quality(const char *value) {
74
  if (!value)
75
    return CHROMA_FILTER_LINEAR; // default
76
77
  if (strcasecmp(value, "nearest") == 0) {
78
    return CHROMA_FILTER_NEAREST;
79
  } else if (strcasecmp(value, "linear") == 0) {
80
    return CHROMA_FILTER_LINEAR;
81
  } else if (strcasecmp(value, "bilinear") == 0) {
82
    return CHROMA_FILTER_BILINEAR;
83
  } else if (strcasecmp(value, "trilinear") == 0) {
84
    return CHROMA_FILTER_TRILINEAR;
85
  }
86
87
  chroma_log("WARN", "Unknown filter quality: %s (using linear)", value);
88
  return CHROMA_FILTER_LINEAR;
89
}
90
91
// Get string representation of scaling mode
92
static const char *scale_mode_to_string(chroma_scale_mode_t mode) {
93
  switch (mode) {
94
  case CHROMA_SCALE_FILL:
95
    return "fill";
96
  case CHROMA_SCALE_FIT:
97
    return "fit";
98
  case CHROMA_SCALE_STRETCH:
99
    return "stretch";
100
  case CHROMA_SCALE_CENTER:
101
    return "center";
102
  default:
103
    return "unknown";
104
  }
105
}
106
107
// Get string representation of filter quality
108
static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
109
  switch (quality) {
110
  case CHROMA_FILTER_NEAREST:
111
    return "nearest";
112
  case CHROMA_FILTER_LINEAR:
113
    return "linear";
114
  case CHROMA_FILTER_BILINEAR:
115
    return "bilinear";
116
  case CHROMA_FILTER_TRILINEAR:
117
    return "trilinear";
118
  default:
119
    return "unknown";
120
  }
121
}
122
123
// Parse anchor position from string
124
static chroma_anchor_t parse_anchor(const char *value) {
125
  if (!value)
126
    return CHROMA_ANCHOR_CENTER;
127
128
  if (strcasecmp(value, "center") == 0) {
129
    return CHROMA_ANCHOR_CENTER;
130
  } else if (strcasecmp(value, "top") == 0) {
131
    return CHROMA_ANCHOR_TOP;
132
  } else if (strcasecmp(value, "bottom") == 0) {
133
    return CHROMA_ANCHOR_BOTTOM;
134
  } else if (strcasecmp(value, "left") == 0) {
135
    return CHROMA_ANCHOR_LEFT;
136
  } else if (strcasecmp(value, "right") == 0) {
137
    return CHROMA_ANCHOR_RIGHT;
138
  } else if (strcasecmp(value, "top-left") == 0 ||
139
             strcasecmp(value, "topleft") == 0) {
140
    return CHROMA_ANCHOR_TOP_LEFT;
141
  } else if (strcasecmp(value, "top-right") == 0 ||
142
             strcasecmp(value, "topright") == 0) {
143
    return CHROMA_ANCHOR_TOP_RIGHT;
144
  } else if (strcasecmp(value, "bottom-left") == 0 ||
145
             strcasecmp(value, "bottomleft") == 0) {
146
    return CHROMA_ANCHOR_BOTTOM_LEFT;
147
  } else if (strcasecmp(value, "bottom-right") == 0 ||
148
             strcasecmp(value, "bottomright") == 0) {
149
    return CHROMA_ANCHOR_BOTTOM_RIGHT;
150
  }
151
152
  chroma_log("WARN", "Unknown anchor: %s (using center)", value);
153
  return CHROMA_ANCHOR_CENTER;
154
}
155
156
// Get string representation of anchor position
157
static const char *anchor_to_string(chroma_anchor_t anchor) {
158
  switch (anchor) {
159
  case CHROMA_ANCHOR_CENTER:
160
    return "center";
161
  case CHROMA_ANCHOR_TOP:
162
    return "top";
163
  case CHROMA_ANCHOR_BOTTOM:
164
    return "bottom";
165
  case CHROMA_ANCHOR_LEFT:
166
    return "left";
167
  case CHROMA_ANCHOR_RIGHT:
168
    return "right";
169
  case CHROMA_ANCHOR_TOP_LEFT:
170
    return "top-left";
171
  case CHROMA_ANCHOR_TOP_RIGHT:
172
    return "top-right";
173
  case CHROMA_ANCHOR_BOTTOM_LEFT:
174
    return "bottom-left";
175
  case CHROMA_ANCHOR_BOTTOM_RIGHT:
176
    return "bottom-right";
177
  default:
178
    return "unknown";
179
  }
180
}
181
182
// Output-to-image mapping
183
static int add_output_mapping(chroma_config_t *config, const char *output_name,
184
                              const char *image_path,
185
                              chroma_scale_mode_t scale_mode,
186
                              chroma_filter_quality_t filter_quality,
187
                              chroma_anchor_t anchor, float anchor_x,
188
                              float anchor_y) {
189
  if (!config || !output_name || !image_path) {
190
    return CHROMA_ERROR_INIT;
191
  }
192
193
  if (config->mapping_count >= MAX_OUTPUTS) {
194
    chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
195
               MAX_OUTPUTS);
196
    return CHROMA_ERROR_MEMORY;
197
  }
198
199
  // XXX: Validate string lengths to prevent buffer overflow
200
  size_t output_len = strlen(output_name);
201
  size_t path_len = strlen(image_path);
202
203
  if (output_len >= sizeof(config->mappings[0].output_name)) {
204
    chroma_log("ERROR", "Output name too long: %s (max %zu)", output_name,
205
               sizeof(config->mappings[0].output_name) - 1);
206
    return CHROMA_ERROR_CONFIG;
207
  }
208
209
  if (path_len >= sizeof(config->mappings[0].image_path)) {
210
    chroma_log("ERROR", "Image path too long: %s (max %zu)", image_path,
211
               sizeof(config->mappings[0].image_path) - 1);
212
    return CHROMA_ERROR_CONFIG;
213
  }
214
215
  chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
216
217
  strcpy(mapping->output_name, output_name);
218
  strcpy(mapping->image_path, image_path);
219
  mapping->scale_mode = scale_mode;
220
  mapping->filter_quality = filter_quality;
221
  mapping->anchor = anchor;
222
  mapping->anchor_x = anchor_x;
223
  mapping->anchor_y = anchor_y;
224
225
  config->mapping_count++;
226
227
  chroma_log(
228
      "DEBUG",
229
      "Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s @ %.1f,%.1f)",
230
      output_name, image_path, scale_mode_to_string(scale_mode),
231
      filter_quality_to_string(filter_quality), anchor_to_string(anchor),
232
      anchor_x, anchor_y);
233
  chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
234
             config->mapping_count, output_name, image_path, path_len);
235
  return CHROMA_OK;
236
}
237
238
// Initialize configuration with defaults
239
static void init_default_config(chroma_config_t *config) {
240
  if (!config)
241
    return;
242
243
  memset(config, 0, sizeof(chroma_config_t));
244
245
  config->daemon_mode = false;
246
  config->mapping_count = 0;
247
248
  // Set default scaling and filtering
249
  config->default_scale_mode = CHROMA_SCALE_FILL;
250
  config->default_filter_quality = CHROMA_FILTER_LINEAR;
251
  config->default_anchor = CHROMA_ANCHOR_CENTER;
252
  config->default_anchor_x = 50.0f; // center
253
  config->default_anchor_y = 50.0f; // center
254
255
  // Set default downsampling settings
256
  config->enable_downsampling = true; // enable by default, performance etc.
257
  config->max_output_width = 3840;    // 4K width
258
  config->max_output_height = 2160;   // 4K height
259
  config->min_scale_factor = 0.25f;   // don't scale below 25%
260
261
  // Set default image path (can be overridden)
262
  const char *home = getenv("HOME");
263
  if (home) {
264
    snprintf(config->default_image, sizeof(config->default_image),
265
             "%s/.config/chroma/default.jpg", home);
266
  } else {
267
    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
268
  }
269
}
270
271
// Parse a single configuration line
272
static int parse_config_line(chroma_config_t *config, char *line,
273
                             int line_number) {
274
  if (!config || !line) {
275
    return CHROMA_ERROR_INIT;
276
  }
277
278
  // Skip empty lines and comments
279
  char *trimmed = trim_whitespace(line);
280
  if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
281
    return CHROMA_OK;
282
  }
283
284
  // Find the equals sign
285
  char *equals = strchr(trimmed, '=');
286
  if (!equals) {
287
    chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
288
    return CHROMA_OK; // continue parsing
289
  }
290
291
  *equals = '\0';
292
  char *key = trim_whitespace(trimmed);
293
  char *value = trim_whitespace(equals + 1);
294
295
  value = remove_quotes(value);
296
297
  if (*key == '\0' || *value == '\0') {
298
    chroma_log("WARN", "Invalid config line %d: empty key or value",
299
               line_number);
300
    return CHROMA_OK;
301
  }
302
303
  // Parse configuration options
304
  if (strcasecmp(key, "default_image") == 0) {
305
    char *expanded_path = chroma_expand_path(value);
306
    const char *path_to_use = expanded_path ? expanded_path : value;
307
    size_t path_len = strlen(path_to_use);
308
309
    if (path_len >= sizeof(config->default_image)) {
310
      chroma_log("ERROR", "Default image path too long: %s (max %zu)",
311
                 path_to_use, sizeof(config->default_image) - 1);
312
      if (expanded_path) {
313
        free(expanded_path);
314
      }
315
      return CHROMA_ERROR_CONFIG;
316
    }
317
318
    strcpy(config->default_image, path_to_use);
319
320
    if (expanded_path) {
321
      chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
322
      chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
323
                 path_len, expanded_path);
324
      free(expanded_path);
325
    } else {
326
      chroma_log("WARN", "Failed to expand path, using original: %s", value);
327
    }
328
  } else if (strcasecmp(key, "daemon") == 0 ||
329
             strcasecmp(key, "daemon_mode") == 0) {
330
    config->daemon_mode = parse_bool(value);
331
    chroma_log("DEBUG", "Set daemon mode: %s",
332
               config->daemon_mode ? "true" : "false");
333
    chroma_log("TRACE",
334
               "Daemon mode configuration: key='%s', value='%s', parsed=%s",
335
               key, value, config->daemon_mode ? "true" : "false");
336
  } else if (strcasecmp(key, "scale_mode") == 0 ||
337
             strcasecmp(key, "default_scale_mode") == 0) {
338
    config->default_scale_mode = parse_scale_mode(value);
339
    chroma_log("DEBUG", "Set default scale mode: %s",
340
               scale_mode_to_string(config->default_scale_mode));
341
    chroma_log("TRACE",
342
               "Scale mode configuration: key='%s', value='%s', parsed=%s", key,
343
               value, scale_mode_to_string(config->default_scale_mode));
344
  } else if (strcasecmp(key, "filter_quality") == 0 ||
345
             strcasecmp(key, "default_filter_quality") == 0) {
346
    config->default_filter_quality = parse_filter_quality(value);
347
    chroma_log("DEBUG", "Set default filter quality: %s",
348
               filter_quality_to_string(config->default_filter_quality));
349
    chroma_log("TRACE",
350
               "Filter quality configuration: key='%s', value='%s', parsed=%s",
351
               key, value,
352
               filter_quality_to_string(config->default_filter_quality));
353
  } else if (strcasecmp(key, "enable_downsampling") == 0) {
354
    config->enable_downsampling = parse_bool(value);
355
    chroma_log("DEBUG", "Set downsampling: %s",
356
               config->enable_downsampling ? "enabled" : "disabled");
357
  } else if (strcasecmp(key, "anchor") == 0) {
358
    config->default_anchor = parse_anchor(value);
359
    chroma_log("DEBUG", "Set default anchor: %s",
360
               anchor_to_string(config->default_anchor));
361
  } else if (strcasecmp(key, "anchor_x") == 0) {
362
    char *endptr = NULL;
363
    float ax = strtof(value, &endptr);
364
    if (endptr == value || *endptr != '\0') {
365
      chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)",
366
                 value);
367
      config->default_anchor_x = 50.0f;
368
    } else if (ax < 0.0f || ax > 100.0f) {
369
      chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value);
370
      config->default_anchor_x = 50.0f;
371
    } else {
372
      config->default_anchor_x = ax;
373
      chroma_log("DEBUG", "Set default anchor_x: %.1f", ax);
374
    }
375
  } else if (strcasecmp(key, "anchor_y") == 0) {
376
    char *endptr = NULL;
377
    float ay = strtof(value, &endptr);
378
    if (endptr == value || *endptr != '\0') {
379
      chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)",
380
                 value);
381
      config->default_anchor_y = 50.0f;
382
    } else if (ay < 0.0f || ay > 100.0f) {
383
      chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value);
384
      config->default_anchor_y = 50.0f;
385
    } else {
386
      config->default_anchor_y = ay;
387
      chroma_log("DEBUG", "Set default anchor_y: %.1f", ay);
388
    }
389
  } else if (strcasecmp(key, "max_output_width") == 0) {
390
    int width = atoi(value);
391
    if (width > 0 && width <= 16384) { // Reasonable limits
392
      config->max_output_width = width;
393
      chroma_log("DEBUG", "Set max output width: %d", width);
394
    } else {
395
      chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);
396
    }
397
  } else if (strcasecmp(key, "max_output_height") == 0) {
398
    int height = atoi(value);
399
    if (height > 0 && height <= 16384) { // Reasonable limits
400
      config->max_output_height = height;
401
      chroma_log("DEBUG", "Set max output height: %d", height);
402
    } else {
403
      chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);
404
    }
405
  } else if (strcasecmp(key, "min_scale_factor") == 0) {
406
    float factor = atof(value);
407
    if (factor > 0.0f && factor <= 1.0f) { // Valid range
408
      config->min_scale_factor = factor;
409
      chroma_log("DEBUG", "Set minimum scale factor: %.2f", factor);
410
    } else {
411
      chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
412
    }
413
  } else if (strncasecmp(key, "output.", 7) == 0) {
414
    // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
415
    const char *output_name = key + 7;
416
    if (*output_name == '\0') {
417
      chroma_log("WARN", "Invalid output mapping line %d: no output name",
418
                 line_number);
419
      return CHROMA_OK;
420
    }
421
422
    // Check for extended output configuration with properties. Format:
423
    // output.DP-1.scale = fill
424
    // output.DP-1.filter = linear
425
    char *dot = strchr(output_name, '.');
426
    if (dot) {
427
      // This is an output property (scale or filter)
428
      *dot = '\0';
429
      const char *property = dot + 1;
430
431
      // Find existing mapping for this output
432
      chroma_config_mapping_t *mapping = NULL;
433
      for (int i = 0; i < config->mapping_count; i++) {
434
        if (strcmp(config->mappings[i].output_name, output_name) == 0) {
435
          mapping = &config->mappings[i];
436
          break;
437
        }
438
      }
439
440
      if (!mapping) {
441
        chroma_log("WARN", "Output %s not found for property %s (line %d)",
442
                   output_name, property, line_number);
443
        return CHROMA_OK;
444
      }
445
446
      if (strcasecmp(property, "scale") == 0) {
447
        mapping->scale_mode = parse_scale_mode(value);
448
        chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,
449
                   scale_mode_to_string(mapping->scale_mode));
450
      } else if (strcasecmp(property, "filter") == 0) {
451
        mapping->filter_quality = parse_filter_quality(value);
452
        chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
453
                   filter_quality_to_string(mapping->filter_quality));
454
      } else if (strcasecmp(property, "anchor") == 0) {
455
        mapping->anchor = parse_anchor(value);
456
        // Set anchor_x/anchor_y based on named anchor
457
        switch (mapping->anchor) {
458
        case CHROMA_ANCHOR_TOP:
459
          mapping->anchor_x = 50.0f;
460
          mapping->anchor_y = 0.0f;
461
          break;
462
        case CHROMA_ANCHOR_BOTTOM:
463
          mapping->anchor_x = 50.0f;
464
          mapping->anchor_y = 100.0f;
465
          break;
466
        case CHROMA_ANCHOR_LEFT:
467
          mapping->anchor_x = 0.0f;
468
          mapping->anchor_y = 50.0f;
469
          break;
470
        case CHROMA_ANCHOR_RIGHT:
471
          mapping->anchor_x = 100.0f;
472
          mapping->anchor_y = 50.0f;
473
          break;
474
        case CHROMA_ANCHOR_TOP_LEFT:
475
          mapping->anchor_x = 0.0f;
476
          mapping->anchor_y = 0.0f;
477
          break;
478
        case CHROMA_ANCHOR_TOP_RIGHT:
479
          mapping->anchor_x = 100.0f;
480
          mapping->anchor_y = 0.0f;
481
          break;
482
        case CHROMA_ANCHOR_BOTTOM_LEFT:
483
          mapping->anchor_x = 0.0f;
484
          mapping->anchor_y = 100.0f;
485
          break;
486
        case CHROMA_ANCHOR_BOTTOM_RIGHT:
487
          mapping->anchor_x = 100.0f;
488
          mapping->anchor_y = 100.0f;
489
          break;
490
        default:
491
          mapping->anchor_x = 50.0f;
492
          mapping->anchor_y = 50.0f;
493
          break;
494
        }
495
        chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",
496
                   output_name, anchor_to_string(mapping->anchor),
497
                   mapping->anchor_x, mapping->anchor_y);
498
      } else if (strcasecmp(property, "anchor_x") == 0) {
499
        float ax = atof(value);
500
        if (ax >= 0.0f && ax <= 100.0f) {
501
          mapping->anchor_x = ax;
502
          chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name,
503
                     ax);
504
        } else {
505
          mapping->anchor_x = 50.0f;
506
          chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)",
507
                     value);
508
        }
509
      } else if (strcasecmp(property, "anchor_y") == 0) {
510
        float ay = atof(value);
511
        if (ay >= 0.0f && ay <= 100.0f) {
512
          mapping->anchor_y = ay;
513
          chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name,
514
                     ay);
515
        } else {
516
          mapping->anchor_y = 50.0f;
517
          chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",
518
                     value);
519
        }
520
      } else {
521
        chroma_log("WARN", "Unknown output property: %s (line %d)", property,
522
                   line_number);
523
      }
524
525
      return CHROMA_OK;
526
    }
527
528
    // Expand path before validation and storage
529
    char *expanded_path = chroma_expand_path(value);
530
    const char *path_to_validate = expanded_path ? expanded_path : value;
531
532
    // Validate image path
533
    if (chroma_image_validate(path_to_validate) != CHROMA_OK) {
534
      chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",
535
                 output_name, value, path_to_validate);
536
      if (expanded_path) {
537
        free(expanded_path);
538
      }
539
      return CHROMA_OK;
540
    }
541
542
    if (add_output_mapping(
543
            config, output_name, path_to_validate, config->default_scale_mode,
544
            config->default_filter_quality, config->default_anchor,
545
            config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) {
546
      chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
547
                 path_to_validate);
548
      if (expanded_path) {
549
        free(expanded_path);
550
      }
551
      return CHROMA_ERROR_CONFIG;
552
    }
553
554
    if (expanded_path) {
555
      free(expanded_path);
556
    }
557
  } else {
558
    chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
559
               key);
560
    chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
561
               line_number, key, value);
562
  }
563
564
  return CHROMA_OK;
565
}
566
567
// Load configuration from file
568
int chroma_config_load(chroma_config_t *config, const char *config_file) {
569
  if (!config) {
570
    return CHROMA_ERROR_INIT;
571
  }
572
573
  // Initialize with defaults
574
  init_default_config(config);
575
576
  if (!config_file) {
577
    chroma_log("INFO", "No config file specified, using defaults");
578
    return CHROMA_OK;
579
  }
580
581
  FILE *file = fopen(config_file, "r");
582
  if (!file) {
583
    if (errno == ENOENT) {
584
      chroma_log("INFO", "Config file not found: %s (using defaults)",
585
                 config_file);
586
      return CHROMA_OK;
587
    } else {
588
      chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
589
                 strerror(errno));
590
      return CHROMA_ERROR_CONFIG;
591
    }
592
  }
593
594
  chroma_log("INFO", "Loading configuration from: %s", config_file);
595
  chroma_log("TRACE",
596
             "Starting configuration parsing, estimated config size: %ld bytes",
597
             chroma_get_file_size(config_file));
598
599
  char line[1024];
600
  int line_number = 0;
601
  int parse_errors = 0;
602
603
  while (fgets(line, sizeof(line), file)) {
604
    line_number++;
605
606
    char *newline = strchr(line, '\n');
607
    if (newline) {
608
      *newline = '\0';
609
    }
610
611
    if (parse_config_line(config, line, line_number) != CHROMA_OK) {
612
      parse_errors++;
613
    }
614
  }
615
616
  fclose(file);
617
618
  if (parse_errors > 0) {
619
    chroma_log("WARN", "Config file contained %d errors", parse_errors);
620
    // Continue anyway with partial configuration
621
  }
622
623
  chroma_log("INFO",
624
             "Loaded configuration: %d output mappings, default image: %s",
625
             config->mapping_count, config->default_image);
626
627
  // Log configuration memory usage
628
  size_t config_size =
629
      sizeof(chroma_config_t) +
630
      (config->mapping_count * sizeof(chroma_config_mapping_t));
631
  chroma_log_resource_allocation("config_data", config_size,
632
                                 "configuration structure");
633
  chroma_log_memory_stats("post-config-load");
634
635
  return CHROMA_OK;
636
}
637
638
// Free configuration resources
639
void chroma_config_free(chroma_config_t *config) {
640
  if (!config) {
641
    return;
642
  }
643
644
  // Log configuration deallocation
645
  size_t config_size =
646
      sizeof(chroma_config_t) +
647
      (config->mapping_count * sizeof(chroma_config_mapping_t));
648
  chroma_log_resource_deallocation("config_data", config_size,
649
                                   "configuration structure");
650
651
  chroma_log("TRACE", "Clearing %d output mappings from configuration",
652
             config->mapping_count);
653
  memset(config->mappings, 0, sizeof(config->mappings));
654
  config->mapping_count = 0;
655
656
  memset(config->default_image, 0, sizeof(config->default_image));
657
658
  chroma_log("DEBUG", "Freed configuration");
659
}
660
661
// Get image path for specific output
662
const char *chroma_config_get_image_for_output(chroma_config_t *config,
663
                                               const char *output_name) {
664
  if (!config || !output_name) {
665
    return NULL;
666
  }
667
668
  // Look for specific output mapping
669
  for (int i = 0; i < config->mapping_count; i++) {
670
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
671
      chroma_log("DEBUG", "Found specific mapping for output %s: %s",
672
                 output_name, config->mappings[i].image_path);
673
      return config->mappings[i].image_path;
674
    }
675
  }
676
677
  // Return default image if no specific mapping found
678
  if (strlen(config->default_image) > 0) {
679
    chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
680
               config->default_image);
681
    return config->default_image;
682
  }
683
684
  chroma_log("WARN", "No image configured for output: %s", output_name);
685
  return NULL;
686
}
687
688
// Get configuration mapping for output, including scale mode, filter
689
// quality, anchor, and custom anchor coordinates
690
int chroma_config_get_mapping_for_output(
691
    chroma_config_t *config, const char *output_name,
692
    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality,
693
    chroma_anchor_t *anchor, float *anchor_x, float *anchor_y) {
694
  if (!config || !output_name || !scale_mode || !filter_quality || !anchor ||
695
      !anchor_x || !anchor_y) {
696
    return CHROMA_ERROR_INIT;
697
  }
698
699
  // Look for specific output mapping
700
  for (int i = 0; i < config->mapping_count; i++) {
701
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
702
      *scale_mode = config->mappings[i].scale_mode;
703
      *filter_quality = config->mappings[i].filter_quality;
704
      *anchor = config->mappings[i].anchor;
705
      *anchor_x = config->mappings[i].anchor_x;
706
      *anchor_y = config->mappings[i].anchor_y;
707
      chroma_log("DEBUG",
708
                 "Found specific mapping for output %s: scale=%s, filter=%s, "
709
                 "anchor=%s @ %.1f,%.1f",
710
                 output_name, scale_mode_to_string(*scale_mode),
711
                 filter_quality_to_string(*filter_quality),
712
                 anchor_to_string(*anchor), *anchor_x, *anchor_y);
713
      return CHROMA_OK;
714
    }
715
  }
716
717
  // Return defaults if no specific mapping found
718
  *scale_mode = config->default_scale_mode;
719
  *filter_quality = config->default_filter_quality;
720
  *anchor = config->default_anchor;
721
  *anchor_x = config->default_anchor_x;
722
  *anchor_y = config->default_anchor_y;
723
  chroma_log("DEBUG",
724
             "Using defaults for output %s: scale=%s, filter=%s, anchor=%s @ "
725
             "%.1f,%.1f",
726
             output_name, scale_mode_to_string(*scale_mode),
727
             filter_quality_to_string(*filter_quality),
728
             anchor_to_string(*anchor), *anchor_x, *anchor_y);
729
  return CHROMA_OK;
730
}
731
732
// Print current configuration for debugging
733
void chroma_config_print(const chroma_config_t *config) {
734
  if (!config) {
735
    return;
736
  }
737
738
  chroma_log("INFO", "=== Configuration ===");
739
  chroma_log("INFO", "Default image: %s", config->default_image);
740
  chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
741
  chroma_log("INFO", "Default scale mode: %s",
742
             scale_mode_to_string(config->default_scale_mode));
743
  chroma_log("INFO", "Default filter quality: %s",
744
             filter_quality_to_string(config->default_filter_quality));
745
  chroma_log("INFO", "Downsampling: %s",
746
             config->enable_downsampling ? "enabled" : "disabled");
747
  if (config->enable_downsampling) {
748
    chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
749
               config->max_output_height);
750
    chroma_log("INFO", "Min scale factor: %.2f", config->min_scale_factor);
751
  }
752
  chroma_log("INFO", "Output mappings: %d", config->mapping_count);
753
754
  for (int i = 0; i < config->mapping_count; i++) {
755
    chroma_log("INFO", "  %s -> %s (scale: %s, filter: %s)",
756
               config->mappings[i].output_name, config->mappings[i].image_path,
757
               scale_mode_to_string(config->mappings[i].scale_mode),
758
               filter_quality_to_string(config->mappings[i].filter_quality));
759
    chroma_log(
760
        "TRACE",
761
        "  Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
762
        "path_exists=%s",
763
        i, config->mappings[i].output_name, config->mappings[i].image_path,
764
        scale_mode_to_string(config->mappings[i].scale_mode),
765
        filter_quality_to_string(config->mappings[i].filter_quality),
766
        chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
767
  }
768
  chroma_log("INFO", "====================");
769
}