brewery
notashelf /
7306bbc6257b080167142f9fc2d8335168d352ac

chroma

public

Lightweight wallpaper daemon for Wayland

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