brewery
notashelf /
ebdc3fe08e4cdde737abc0080b067fd6c86e093c

chroma

public

Lightweight wallpaper daemon for Wayland

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