brewery
notashelf /
ecff681d373bd0973a027d43b1fe36fb2ab15059

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git
src/config.cC582 lines18.9 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 image path (can be overridden)
188
  const char *home = getenv("HOME");
189
  if (home) {
190
    snprintf(config->default_image, sizeof(config->default_image),
191
             "%s/.config/chroma/default.jpg", home);
192
  } else {
193
    strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
194
  }
195
}
196
197
// Parse a single configuration line
198
static int parse_config_line(chroma_config_t *config, char *line,
199
                             int line_number) {
200
  if (!config || !line) {
201
    return CHROMA_ERROR_INIT;
202
  }
203
204
  // Skip empty lines and comments
205
  char *trimmed = trim_whitespace(line);
206
  if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
207
    return CHROMA_OK;
208
  }
209
210
  // Find the equals sign
211
  char *equals = strchr(trimmed, '=');
212
  if (!equals) {
213
    chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
214
    return CHROMA_OK; // continue parsing
215
  }
216
217
  *equals = '\0';
218
  char *key = trim_whitespace(trimmed);
219
  char *value = trim_whitespace(equals + 1);
220
221
  value = remove_quotes(value);
222
223
  if (*key == '\0' || *value == '\0') {
224
    chroma_log("WARN", "Invalid config line %d: empty key or value",
225
               line_number);
226
    return CHROMA_OK;
227
  }
228
229
  // Parse configuration options
230
  if (strcasecmp(key, "default_image") == 0) {
231
    char *expanded_path = chroma_expand_path(value);
232
    const char *path_to_use = expanded_path ? expanded_path : value;
233
    size_t path_len = strlen(path_to_use);
234
235
    if (path_len >= sizeof(config->default_image)) {
236
      chroma_log("ERROR", "Default image path too long: %s (max %zu)",
237
                 path_to_use, sizeof(config->default_image) - 1);
238
      if (expanded_path) {
239
        free(expanded_path);
240
      }
241
      return CHROMA_ERROR_CONFIG;
242
    }
243
244
    strcpy(config->default_image, path_to_use);
245
246
    if (expanded_path) {
247
      chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
248
      chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
249
                 path_len, expanded_path);
250
      free(expanded_path);
251
    } else {
252
      chroma_log("WARN", "Failed to expand path, using original: %s", value);
253
    }
254
  } else if (strcasecmp(key, "daemon") == 0 ||
255
             strcasecmp(key, "daemon_mode") == 0) {
256
    config->daemon_mode = parse_bool(value);
257
    chroma_log("DEBUG", "Set daemon mode: %s",
258
               config->daemon_mode ? "true" : "false");
259
    chroma_log("TRACE",
260
               "Daemon mode configuration: key='%s', value='%s', parsed=%s",
261
               key, value, config->daemon_mode ? "true" : "false");
262
  } else if (strcasecmp(key, "scale_mode") == 0 ||
263
             strcasecmp(key, "default_scale_mode") == 0) {
264
    config->default_scale_mode = parse_scale_mode(value);
265
    chroma_log("DEBUG", "Set default scale mode: %s",
266
               scale_mode_to_string(config->default_scale_mode));
267
    chroma_log("TRACE",
268
               "Scale mode configuration: key='%s', value='%s', parsed=%s", key,
269
               value, scale_mode_to_string(config->default_scale_mode));
270
  } else if (strcasecmp(key, "filter_quality") == 0 ||
271
             strcasecmp(key, "default_filter_quality") == 0) {
272
    config->default_filter_quality = parse_filter_quality(value);
273
    chroma_log("DEBUG", "Set default filter quality: %s",
274
               filter_quality_to_string(config->default_filter_quality));
275
    chroma_log("TRACE",
276
               "Filter quality configuration: key='%s', value='%s', parsed=%s",
277
               key, value,
278
               filter_quality_to_string(config->default_filter_quality));
279
  } else if (strncasecmp(key, "output.", 7) == 0) {
280
    // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
281
    const char *output_name = key + 7;
282
    if (*output_name == '\0') {
283
      chroma_log("WARN", "Invalid output mapping line %d: no output name",
284
                 line_number);
285
      return CHROMA_OK;
286
    }
287
288
    // Check for extended output configuration with properties
289
    // Format: output.DP-1.scale = fill
290
    // Format: output.DP-1.filter = linear
291
    char *dot = strchr(output_name, '.');
292
    if (dot) {
293
      // This is an output property (scale or filter)
294
      *dot = '\0';
295
      const char *property = dot + 1;
296
297
      // Find existing mapping for this output
298
      chroma_config_mapping_t *mapping = NULL;
299
      for (int i = 0; i < config->mapping_count; i++) {
300
        if (strcmp(config->mappings[i].output_name, output_name) == 0) {
301
          mapping = &config->mappings[i];
302
          break;
303
        }
304
      }
305
306
      if (!mapping) {
307
        chroma_log("WARN", "Output %s not found for property %s (line %d)",
308
                   output_name, property, line_number);
309
        return CHROMA_OK;
310
      }
311
312
      if (strcasecmp(property, "scale") == 0) {
313
        mapping->scale_mode = parse_scale_mode(value);
314
        chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,
315
                   scale_mode_to_string(mapping->scale_mode));
316
      } else if (strcasecmp(property, "filter") == 0) {
317
        mapping->filter_quality = parse_filter_quality(value);
318
        chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
319
                   filter_quality_to_string(mapping->filter_quality));
320
      } else {
321
        chroma_log("WARN", "Unknown output property: %s (line %d)", property,
322
                   line_number);
323
      }
324
325
      return CHROMA_OK;
326
    }
327
328
    // Expand path before validation and storage
329
    char *expanded_path = chroma_expand_path(value);
330
    const char *path_to_validate = expanded_path ? expanded_path : value;
331
332
    // Validate image path
333
    if (chroma_image_validate(path_to_validate) != CHROMA_OK) {
334
      chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",
335
                 output_name, value, path_to_validate);
336
      if (expanded_path) {
337
        free(expanded_path);
338
      }
339
      return CHROMA_OK;
340
    }
341
342
    if (add_output_mapping(config, output_name, path_to_validate,
343
                           config->default_scale_mode,
344
                           config->default_filter_quality) != CHROMA_OK) {
345
      chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
346
                 path_to_validate);
347
      if (expanded_path) {
348
        free(expanded_path);
349
      }
350
      return CHROMA_ERROR_CONFIG;
351
    }
352
353
    if (expanded_path) {
354
      free(expanded_path);
355
    }
356
  } else {
357
    chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
358
               key);
359
    chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
360
               line_number, key, value);
361
  }
362
363
  return CHROMA_OK;
364
}
365
366
// Load configuration from file
367
int chroma_config_load(chroma_config_t *config, const char *config_file) {
368
  if (!config) {
369
    return CHROMA_ERROR_INIT;
370
  }
371
372
  // Initialize with defaults
373
  init_default_config(config);
374
375
  if (!config_file) {
376
    chroma_log("INFO", "No config file specified, using defaults");
377
    return CHROMA_OK;
378
  }
379
380
  FILE *file = fopen(config_file, "r");
381
  if (!file) {
382
    if (errno == ENOENT) {
383
      chroma_log("INFO", "Config file not found: %s (using defaults)",
384
                 config_file);
385
      return CHROMA_OK;
386
    } else {
387
      chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
388
                 strerror(errno));
389
      return CHROMA_ERROR_CONFIG;
390
    }
391
  }
392
393
  chroma_log("INFO", "Loading configuration from: %s", config_file);
394
  chroma_log("TRACE",
395
             "Starting configuration parsing, estimated config size: %ld bytes",
396
             chroma_get_file_size(config_file));
397
398
  char line[1024];
399
  int line_number = 0;
400
  int parse_errors = 0;
401
402
  while (fgets(line, sizeof(line), file)) {
403
    line_number++;
404
405
    char *newline = strchr(line, '\n');
406
    if (newline) {
407
      *newline = '\0';
408
    }
409
410
    if (parse_config_line(config, line, line_number) != CHROMA_OK) {
411
      parse_errors++;
412
    }
413
  }
414
415
  fclose(file);
416
417
  if (parse_errors > 0) {
418
    chroma_log("WARN", "Config file contained %d errors", parse_errors);
419
    // Continue anyway with partial configuration
420
  }
421
422
  chroma_log("INFO",
423
             "Loaded configuration: %d output mappings, default image: %s",
424
             config->mapping_count, config->default_image);
425
426
  // Log configuration memory usage
427
  size_t config_size =
428
      sizeof(chroma_config_t) +
429
      (config->mapping_count * sizeof(chroma_config_mapping_t));
430
  chroma_log_resource_allocation("config_data", config_size,
431
                                 "configuration structure");
432
  chroma_log_memory_stats("post-config-load");
433
434
  return CHROMA_OK;
435
}
436
437
// Free configuration resources
438
void chroma_config_free(chroma_config_t *config) {
439
  if (!config) {
440
    return;
441
  }
442
443
  // Log configuration deallocation
444
  size_t config_size =
445
      sizeof(chroma_config_t) +
446
      (config->mapping_count * sizeof(chroma_config_mapping_t));
447
  chroma_log_resource_deallocation("config_data", config_size,
448
                                   "configuration structure");
449
450
  chroma_log("TRACE", "Clearing %d output mappings from configuration",
451
             config->mapping_count);
452
  memset(config->mappings, 0, sizeof(config->mappings));
453
  config->mapping_count = 0;
454
455
  memset(config->default_image, 0, sizeof(config->default_image));
456
457
  chroma_log("DEBUG", "Freed configuration");
458
}
459
460
// Get image path for specific output
461
const char *chroma_config_get_image_for_output(chroma_config_t *config,
462
                                               const char *output_name) {
463
  if (!config || !output_name) {
464
    return NULL;
465
  }
466
467
  // Look for specific output mapping
468
  for (int i = 0; i < config->mapping_count; i++) {
469
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
470
      chroma_log("DEBUG", "Found specific mapping for output %s: %s",
471
                 output_name, config->mappings[i].image_path);
472
      return config->mappings[i].image_path;
473
    }
474
  }
475
476
  // Return default image if no specific mapping found
477
  if (strlen(config->default_image) > 0) {
478
    chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
479
               config->default_image);
480
    return config->default_image;
481
  }
482
483
  chroma_log("WARN", "No image configured for output: %s", output_name);
484
  return NULL;
485
}
486
487
// Get configuration mapping for output, including scale mode and filter
488
// quality
489
int chroma_config_get_mapping_for_output(
490
    chroma_config_t *config, const char *output_name,
491
    chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality) {
492
  if (!config || !output_name || !scale_mode || !filter_quality) {
493
    return CHROMA_ERROR_INIT;
494
  }
495
496
  // Look for specific output mapping
497
  for (int i = 0; i < config->mapping_count; i++) {
498
    if (strcmp(config->mappings[i].output_name, output_name) == 0) {
499
      *scale_mode = config->mappings[i].scale_mode;
500
      *filter_quality = config->mappings[i].filter_quality;
501
      chroma_log("DEBUG",
502
                 "Found specific mapping for output %s: scale=%s, filter=%s",
503
                 output_name, scale_mode_to_string(*scale_mode),
504
                 filter_quality_to_string(*filter_quality));
505
      return CHROMA_OK;
506
    }
507
  }
508
509
  // Return defaults if no specific mapping found
510
  *scale_mode = config->default_scale_mode;
511
  *filter_quality = config->default_filter_quality;
512
  chroma_log("DEBUG", "Using defaults for output %s: scale=%s, filter=%s",
513
             output_name, scale_mode_to_string(*scale_mode),
514
             filter_quality_to_string(*filter_quality));
515
  return CHROMA_OK;
516
}
517
518
// Create a sample configuration file
519
int chroma_config_create_sample(const char *config_file) {
520
  if (!config_file) {
521
    return CHROMA_ERROR_INIT;
522
  }
523
524
  FILE *file = fopen(config_file, "w");
525
  if (!file) {
526
    chroma_log("ERROR", "Failed to create sample config file %s: %s",
527
               config_file, strerror(errno));
528
    return CHROMA_ERROR_CONFIG;
529
  }
530
531
  fprintf(file, "# Chroma Wallpaper Daemon Configuration\n");
532
  fprintf(file, "# Lines starting with # are comments\n\n");
533
534
  fprintf(file, "# Default wallpaper for outputs without specific mapping\n");
535
  fprintf(file, "default_image = ~/.config/chroma/default.jpg\n\n");
536
537
  fprintf(file, "# Output-specific wallpapers\n");
538
  fprintf(file, "# Format: output.OUTPUT_NAME = /path/to/image.jpg\n");
539
  fprintf(file, "# You can find output names using: wlr-randr\n");
540
  fprintf(file, "\n");
541
  fprintf(file, "# Examples:\n");
542
  fprintf(file, "# output.DP-1 = ~/.config/chroma/monitor1.jpg\n");
543
  fprintf(file, "# output.DP-2 = ~/.config/chroma/monitor2.png\n");
544
  fprintf(file, "# output.HDMI-A-1 = ~/.config/chroma/hdmi.jpg\n");
545
546
  fclose(file);
547
548
  chroma_log("INFO", "Created sample configuration file: %s", config_file);
549
  return CHROMA_OK;
550
}
551
552
// Print current configuration for debugging
553
void chroma_config_print(const chroma_config_t *config) {
554
  if (!config) {
555
    return;
556
  }
557
558
  chroma_log("INFO", "=== Configuration ===");
559
  chroma_log("INFO", "Default image: %s", config->default_image);
560
  chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
561
  chroma_log("INFO", "Default scale mode: %s",
562
             scale_mode_to_string(config->default_scale_mode));
563
  chroma_log("INFO", "Default filter quality: %s",
564
             filter_quality_to_string(config->default_filter_quality));
565
  chroma_log("INFO", "Output mappings: %d", config->mapping_count);
566
567
  for (int i = 0; i < config->mapping_count; i++) {
568
    chroma_log("INFO", "  %s -> %s (scale: %s, filter: %s)",
569
               config->mappings[i].output_name, config->mappings[i].image_path,
570
               scale_mode_to_string(config->mappings[i].scale_mode),
571
               filter_quality_to_string(config->mappings[i].filter_quality));
572
    chroma_log(
573
        "TRACE",
574
        "  Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
575
        "path_exists=%s",
576
        i, config->mappings[i].output_name, config->mappings[i].image_path,
577
        scale_mode_to_string(config->mappings[i].scale_mode),
578
        filter_quality_to_string(config->mappings[i].filter_quality),
579
        chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
580
  }
581
  chroma_log("INFO", "====================");
582
}