brewery
notashelf /
d145d88b7ee83cfd47af56afaae79a177b49d436

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/config.cC591 lines19.6 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
// Default configuration values
10
11
static char *trim_whitespace(char *str) {
12
  char *end;
13
14
  // Trim leading whitespace
15
  while (isspace((unsigned char)*str))
16
    str++;
17
18
  // All spaces?
19
  if (*str == 0)
20
    return str;
21
22
  // Trim trailing whitespace
23
  end = str + strlen(str) - 1;
24
  while (end > str && isspace((unsigned char)*end))
25
    end--;
26
27
  *(end + 1) = '\0';
28
  return str;
29
}
30
31
// Remove quotes from a string
32
static char *remove_quotes(char *str) {
33
  size_t len = strlen(str);
34
  if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
35
                   (str[0] == '\'' && str[len - 1] == '\''))) {
36
    str[len - 1] = '\0';
37
    return str + 1;
38
  }
39
  return str;
40
}
41
42
// Parse boolean value from string
43
static bool parse_bool(const char *value) {
44
  if (!value)
45
    return false;
46
47
  if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||
48
      strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {
49
    return true;
50
  }
51
52
  return false;
53
}
54
55
// Parse scaling mode from string
56
static chroma_scale_mode_t parse_scale_mode(const char *value) {
57
  if (!value)
58
    return CHROMA_SCALE_FILL; // default
59
60
  if (strcasecmp(value, "fill") == 0) {
61
    return CHROMA_SCALE_FILL;
62
  } else if (strcasecmp(value, "fit") == 0) {
63
    return CHROMA_SCALE_FIT;
64
  } else if (strcasecmp(value, "stretch") == 0) {
65
    return CHROMA_SCALE_STRETCH;
66
  } else if (strcasecmp(value, "center") == 0) {
67
    return CHROMA_SCALE_CENTER;
68
  }
69
70
  chroma_log("WARN", "Unknown scaling mode: %s (using fill)", value);
71
  return CHROMA_SCALE_FILL;
72
}
73
74
// Parse filter quality from string
75
static chroma_filter_quality_t parse_filter_quality(const char *value) {
76
  if (!value)
77
    return CHROMA_FILTER_LINEAR; // default
78
79
  if (strcasecmp(value, "nearest") == 0) {
80
    return CHROMA_FILTER_NEAREST;
81
  } else if (strcasecmp(value, "linear") == 0) {
82
    return CHROMA_FILTER_LINEAR;
83
  } else if (strcasecmp(value, "bilinear") == 0) {
84
    return CHROMA_FILTER_BILINEAR;
85
  } else if (strcasecmp(value, "trilinear") == 0) {
86
    return CHROMA_FILTER_TRILINEAR;
87
  }
88
89
  chroma_log("WARN", "Unknown filter quality: %s (using linear)", value);
90
  return CHROMA_FILTER_LINEAR;
91
}
92
93
// Get string representation of scaling mode
94
static const char *scale_mode_to_string(chroma_scale_mode_t mode) {
95
  switch (mode) {
96
  case CHROMA_SCALE_FILL:
97
    return "fill";
98
  case CHROMA_SCALE_FIT:
99
    return "fit";
100
  case CHROMA_SCALE_STRETCH:
101
    return "stretch";
102
  case CHROMA_SCALE_CENTER:
103
    return "center";
104
  default:
105
    return "unknown";
106
  }
107
}
108
109
// Get string representation of filter quality
110
static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
111
  switch (quality) {
112
  case CHROMA_FILTER_NEAREST:
113
    return "nearest";
114
  case CHROMA_FILTER_LINEAR:
115
    return "linear";
116
  case CHROMA_FILTER_BILINEAR:
117
    return "bilinear";
118
  case CHROMA_FILTER_TRILINEAR:
119
    return "trilinear";
120
  default:
121
    return "unknown";
122
  }
123
}
124
125
// Output-to-image mapping
126
static int add_output_mapping(chroma_config_t *config, const char *output_name,
127
                              const char *image_path,
128
                              chroma_scale_mode_t scale_mode,
129
                              chroma_filter_quality_t filter_quality) {
130
  if (!config || !output_name || !image_path) {
131
    return CHROMA_ERROR_INIT;
132
  }
133
134
  if (config->mapping_count >= MAX_OUTPUTS) {
135
    chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
136
               MAX_OUTPUTS);
137
    return CHROMA_ERROR_MEMORY;
138
  }
139
140
  // Validate string lengths to prevent buffer overflow
141
  size_t output_len = strlen(output_name);
142
  size_t path_len = strlen(image_path);
143
144
  if (output_len >= sizeof(config->mappings[0].output_name)) {
145
    chroma_log("ERROR", "Output name too long: %s (max %zu)", output_name,
146
               sizeof(config->mappings[0].output_name) - 1);
147
    return CHROMA_ERROR_CONFIG;
148
  }
149
150
  if (path_len >= sizeof(config->mappings[0].image_path)) {
151
    chroma_log("ERROR", "Image path too long: %s (max %zu)", image_path,
152
               sizeof(config->mappings[0].image_path) - 1);
153
    return CHROMA_ERROR_CONFIG;
154
  }
155
156
  chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
157
158
  strcpy(mapping->output_name, output_name);
159
  strcpy(mapping->image_path, image_path);
160
  mapping->scale_mode = scale_mode;
161
  mapping->filter_quality = filter_quality;
162
163
  config->mapping_count++;
164
165
  chroma_log("DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s)",
166
             output_name, image_path, scale_mode_to_string(scale_mode),
167
             filter_quality_to_string(filter_quality));
168
  chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
169
             config->mapping_count, output_name, image_path, path_len);
170
  return CHROMA_OK;
171
}
172
173
// Initialize configuration with defaults
174
static void init_default_config(chroma_config_t *config) {
175
  if (!config)
176
    return;
177
178
  memset(config, 0, sizeof(chroma_config_t));
179
180
  config->daemon_mode = false;
181
  config->mapping_count = 0;
182
183
  // Set default scaling and filtering
184
  config->default_scale_mode = CHROMA_SCALE_FILL;
185
  config->default_filter_quality = CHROMA_FILTER_LINEAR;
186
187
  // Set default downsampling settings
188
  config->enable_downsampling = true; // enable by default, performance etc.
189
  config->max_output_width = 3840;    // 4K width
190
  config->max_output_height = 2160;   // 4K height
191
  config->min_scale_factor = 0.25f;   // don't scale below 25%
192
193
  // Set default image path (can be overridden)
194
  const char *home = getenv("HOME");
195
  if (home) {
196
    snprintf(config->default_image, sizeof(config->default_image),
197
             "%s/.config/chroma/default.jpg", home);
198
  } else {
199
    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
200
  }
201
}
202
203
// Parse a single configuration line
204
static int parse_config_line(chroma_config_t *config, char *line,
205
                             int line_number) {
206
  if (!config || !line) {
207
    return CHROMA_ERROR_INIT;
208
  }
209
210
  // Skip empty lines and comments
211
  char *trimmed = trim_whitespace(line);
212
  if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
213
    return CHROMA_OK;
214
  }
215
216
  // Find the equals sign
217
  char *equals = strchr(trimmed, '=');
218
  if (!equals) {
219
    chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
220
    return CHROMA_OK; // continue parsing
221
  }
222
223
  *equals = '\0';
224
  char *key = trim_whitespace(trimmed);
225
  char *value = trim_whitespace(equals + 1);
226
227
  value = remove_quotes(value);
228
229
  if (*key == '\0' || *value == '\0') {
230
    chroma_log("WARN", "Invalid config line %d: empty key or value",
231
               line_number);
232
    return CHROMA_OK;
233
  }
234
235
  // Parse configuration options
236
  if (strcasecmp(key, "default_image") == 0) {
237
    char *expanded_path = chroma_expand_path(value);
238
    const char *path_to_use = expanded_path ? expanded_path : value;
239
    size_t path_len = strlen(path_to_use);
240
241
    if (path_len >= sizeof(config->default_image)) {
242
      chroma_log("ERROR", "Default image path too long: %s (max %zu)",
243
                 path_to_use, sizeof(config->default_image) - 1);
244
      if (expanded_path) {
245
        free(expanded_path);
246
      }
247
      return CHROMA_ERROR_CONFIG;
248
    }
249
250
    strcpy(config->default_image, path_to_use);
251
252
    if (expanded_path) {
253
      chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
254
      chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
255
                 path_len, expanded_path);
256
      free(expanded_path);
257
    } else {
258
      chroma_log("WARN", "Failed to expand path, using original: %s", value);
259
    }
260
  } else if (strcasecmp(key, "daemon") == 0 ||
261
             strcasecmp(key, "daemon_mode") == 0) {
262
    config->daemon_mode = parse_bool(value);
263
    chroma_log("DEBUG", "Set daemon mode: %s",
264
               config->daemon_mode ? "true" : "false");
265
    chroma_log("TRACE",
266
               "Daemon mode configuration: key='%s', value='%s', parsed=%s",
267
               key, value, config->daemon_mode ? "true" : "false");
268
  } else if (strcasecmp(key, "scale_mode") == 0 ||
269
             strcasecmp(key, "default_scale_mode") == 0) {
270
    config->default_scale_mode = parse_scale_mode(value);
271
    chroma_log("DEBUG", "Set default scale mode: %s",
272
               scale_mode_to_string(config->default_scale_mode));
273
    chroma_log("TRACE",
274
               "Scale mode configuration: key='%s', value='%s', parsed=%s", key,
275
               value, scale_mode_to_string(config->default_scale_mode));
276
  } else if (strcasecmp(key, "filter_quality") == 0 ||
277
             strcasecmp(key, "default_filter_quality") == 0) {
278
    config->default_filter_quality = parse_filter_quality(value);
279
    chroma_log("DEBUG", "Set default filter quality: %s",
280
               filter_quality_to_string(config->default_filter_quality));
281
    chroma_log("TRACE",
282
               "Filter quality configuration: key='%s', value='%s', parsed=%s",
283
               key, value,
284
               filter_quality_to_string(config->default_filter_quality));
285
  } else if (strcasecmp(key, "enable_downsampling") == 0) {
286
    config->enable_downsampling = parse_bool(value);
287
    chroma_log("DEBUG", "Set downsampling: %s",
288
               config->enable_downsampling ? "enabled" : "disabled");
289
  } else if (strcasecmp(key, "max_output_width") == 0) {
290
    int width = atoi(value);
291
    if (width > 0 && width <= 16384) { // Reasonable limits
292
      config->max_output_width = width;
293
      chroma_log("DEBUG", "Set max output width: %d", width);
294
    } else {
295
      chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);
296
    }
297
  } else if (strcasecmp(key, "max_output_height") == 0) {
298
    int height = atoi(value);
299
    if (height > 0 && height <= 16384) { // Reasonable limits
300
      config->max_output_height = height;
301
      chroma_log("DEBUG", "Set max output height: %d", height);
302
    } else {
303
      chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);
304
    }
305
  } else if (strcasecmp(key, "min_scale_factor") == 0) {
306
    float factor = atof(value);
307
    if (factor > 0.0f && factor <= 1.0f) { // Valid range
308
      config->min_scale_factor = factor;
309
      chroma_log("DEBUG", "Set minimum scale factor: %.2f", factor);
310
    } else {
311
      chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
312
    }
313
  } else if (strncasecmp(key, "output.", 7) == 0) {
314
    // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
315
    const char *output_name = key + 7;
316
    if (*output_name == '\0') {
317
      chroma_log("WARN", "Invalid output mapping line %d: no output name",
318
                 line_number);
319
      return CHROMA_OK;
320
    }
321
322
    // Check for extended output configuration with properties
323
    // Format: output.DP-1.scale = fill
324
    // Format: output.DP-1.filter = linear
325
    char *dot = strchr(output_name, '.');
326
    if (dot) {
327
      // This is an output property (scale or filter)
328
      *dot = '\0';
329
      const char *property = dot + 1;
330
331
      // Find existing mapping for this output
332
      chroma_config_mapping_t *mapping = NULL;
333
      for (int i = 0; i < config->mapping_count; i++) {
334
        if (strcmp(config->mappings[i].output_name, output_name) == 0) {
335
          mapping = &config->mappings[i];
336
          break;
337
        }
338
      }
339
340
      if (!mapping) {
341
        chroma_log("WARN", "Output %s not found for property %s (line %d)",
342
                   output_name, property, line_number);
343
        return CHROMA_OK;
344
      }
345
346
      if (strcasecmp(property, "scale") == 0) {
347
        mapping->scale_mode = parse_scale_mode(value);
348
        chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,
349
                   scale_mode_to_string(mapping->scale_mode));
350
      } else if (strcasecmp(property, "filter") == 0) {
351
        mapping->filter_quality = parse_filter_quality(value);
352
        chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
353
                   filter_quality_to_string(mapping->filter_quality));
354
      } else {
355
        chroma_log("WARN", "Unknown output property: %s (line %d)", property,
356
                   line_number);
357
      }
358
359
      return CHROMA_OK;
360
    }
361
362
    // Expand path before validation and storage
363
    char *expanded_path = chroma_expand_path(value);
364
    const char *path_to_validate = expanded_path ? expanded_path : value;
365
366
    // Validate image path
367
    if (chroma_image_validate(path_to_validate) != CHROMA_OK) {
368
      chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",
369
                 output_name, value, path_to_validate);
370
      if (expanded_path) {
371
        free(expanded_path);
372
      }
373
      return CHROMA_OK;
374
    }
375
376
    if (add_output_mapping(config, output_name, path_to_validate,
377
                           config->default_scale_mode,
378
                           config->default_filter_quality) != CHROMA_OK) {
379
      chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
380
                 path_to_validate);
381
      if (expanded_path) {
382
        free(expanded_path);
383
      }
384
      return CHROMA_ERROR_CONFIG;
385
    }
386
387
    if (expanded_path) {
388
      free(expanded_path);
389
    }
390
  } else {
391
    chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
392
               key);
393
    chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
394
               line_number, key, value);
395
  }
396
397
  return CHROMA_OK;
398
}
399
400
// Load configuration from file
401
int chroma_config_load(chroma_config_t *config, const char *config_file) {
402
  if (!config) {
403
    return CHROMA_ERROR_INIT;
404
  }
405
406
  // Initialize with defaults
407
  init_default_config(config);
408
409
  if (!config_file) {
410
    chroma_log("INFO", "No config file specified, using defaults");
411
    return CHROMA_OK;
412
  }
413
414
  FILE *file = fopen(config_file, "r");
415
  if (!file) {
416
    if (errno == ENOENT) {
417
      chroma_log("INFO", "Config file not found: %s (using defaults)",
418
                 config_file);
419
      return CHROMA_OK;
420
    } else {
421
      chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
422
                 strerror(errno));
423
      return CHROMA_ERROR_CONFIG;
424
    }
425
  }
426
427
  chroma_log("INFO", "Loading configuration from: %s", config_file);
428
  chroma_log("TRACE",
429
             "Starting configuration parsing, estimated config size: %ld bytes",
430
             chroma_get_file_size(config_file));
431
432
  char line[1024];
433
  int line_number = 0;
434
  int parse_errors = 0;
435
436
  while (fgets(line, sizeof(line), file)) {
437
    line_number++;
438
439
    char *newline = strchr(line, '\n');
440
    if (newline) {
441
      *newline = '\0';
442
    }
443
444
    if (parse_config_line(config, line, line_number) != CHROMA_OK) {
445
      parse_errors++;
446
    }
447
  }
448
449
  fclose(file);
450
451
  if (parse_errors > 0) {
452
    chroma_log("WARN", "Config file contained %d errors", parse_errors);
453
    // Continue anyway with partial configuration
454
  }
455
456
  chroma_log("INFO",
457
             "Loaded configuration: %d output mappings, default image: %s",
458
             config->mapping_count, config->default_image);
459
460
  // Log configuration memory usage
461
  size_t config_size =
462
      sizeof(chroma_config_t) +
463
      (config->mapping_count * sizeof(chroma_config_mapping_t));
464
  chroma_log_resource_allocation("config_data", config_size,
465
                                 "configuration structure");
466
  chroma_log_memory_stats("post-config-load");
467
468
  return CHROMA_OK;
469
}
470
471
// Free configuration resources
472
void chroma_config_free(chroma_config_t *config) {
473
  if (!config) {
474
    return;
475
  }
476
477
  // Log configuration deallocation
478
  size_t config_size =
479
      sizeof(chroma_config_t) +
480
      (config->mapping_count * sizeof(chroma_config_mapping_t));
481
  chroma_log_resource_deallocation("config_data", config_size,
482
                                   "configuration structure");
483
484
  chroma_log("TRACE", "Clearing %d output mappings from configuration",
485
             config->mapping_count);
486
  memset(config->mappings, 0, sizeof(config->mappings));
487
  config->mapping_count = 0;
488
489
  memset(config->default_image, 0, sizeof(config->default_image));
490
491
  chroma_log("DEBUG", "Freed configuration");
492
}
493
494
// Get image path for specific output
495
const char *chroma_config_get_image_for_output(chroma_config_t *config,
496
                                               const char *output_name) {
497
  if (!config || !output_name) {
498
    return NULL;
499
  }
500
501
  // Look for specific output mapping
502
  for (int i = 0; i < config->mapping_count; i++) {
503
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
504
      chroma_log("DEBUG", "Found specific mapping for output %s: %s",
505
                 output_name, config->mappings[i].image_path);
506
      return config->mappings[i].image_path;
507
    }
508
  }
509
510
  // Return default image if no specific mapping found
511
  if (strlen(config->default_image) > 0) {
512
    chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
513
               config->default_image);
514
    return config->default_image;
515
  }
516
517
  chroma_log("WARN", "No image configured for output: %s", output_name);
518
  return NULL;
519
}
520
521
// Get configuration mapping for output, including scale mode and filter
522
// quality
523
int chroma_config_get_mapping_for_output(
524
    chroma_config_t *config, const char *output_name,
525
    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality) {
526
  if (!config || !output_name || !scale_mode || !filter_quality) {
527
    return CHROMA_ERROR_INIT;
528
  }
529
530
  // Look for specific output mapping
531
  for (int i = 0; i < config->mapping_count; i++) {
532
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
533
      *scale_mode = config->mappings[i].scale_mode;
534
      *filter_quality = config->mappings[i].filter_quality;
535
      chroma_log("DEBUG",
536
                 "Found specific mapping for output %s: scale=%s, filter=%s",
537
                 output_name, scale_mode_to_string(*scale_mode),
538
                 filter_quality_to_string(*filter_quality));
539
      return CHROMA_OK;
540
    }
541
  }
542
543
  // Return defaults if no specific mapping found
544
  *scale_mode = config->default_scale_mode;
545
  *filter_quality = config->default_filter_quality;
546
  chroma_log("DEBUG", "Using defaults for output %s: scale=%s, filter=%s",
547
             output_name, scale_mode_to_string(*scale_mode),
548
             filter_quality_to_string(*filter_quality));
549
  return CHROMA_OK;
550
}
551
552
553
554
// Print current configuration for debugging
555
void chroma_config_print(const chroma_config_t *config) {
556
  if (!config) {
557
    return;
558
  }
559
560
  chroma_log("INFO", "=== Configuration ===");
561
  chroma_log("INFO", "Default image: %s", config->default_image);
562
  chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
563
  chroma_log("INFO", "Default scale mode: %s",
564
             scale_mode_to_string(config->default_scale_mode));
565
  chroma_log("INFO", "Default filter quality: %s",
566
             filter_quality_to_string(config->default_filter_quality));
567
  chroma_log("INFO", "Downsampling: %s",
568
             config->enable_downsampling ? "enabled" : "disabled");
569
  if (config->enable_downsampling) {
570
    chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
571
               config->max_output_height);
572
    chroma_log("INFO", "Min scale factor: %.2f", config->min_scale_factor);
573
  }
574
  chroma_log("INFO", "Output mappings: %d", config->mapping_count);
575
576
  for (int i = 0; i < config->mapping_count; i++) {
577
    chroma_log("INFO", "  %s -> %s (scale: %s, filter: %s)",
578
               config->mappings[i].output_name, config->mappings[i].image_path,
579
               scale_mode_to_string(config->mappings[i].scale_mode),
580
               filter_quality_to_string(config->mappings[i].filter_quality));
581
    chroma_log(
582
        "TRACE",
583
        "  Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
584
        "path_exists=%s",
585
        i, config->mappings[i].output_name, config->mappings[i].image_path,
586
        scale_mode_to_string(config->mappings[i].scale_mode),
587
        filter_quality_to_string(config->mappings[i].filter_quality),
588
        chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
589
  }
590
  chroma_log("INFO", "====================");
591
}