Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I43c5821edc0e121962bee76e39cb32816a6a6964
591 lines
20 KiB
C
591 lines
20 KiB
C
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "../include/chroma.h"
|
|
|
|
// Default configuration values
|
|
|
|
static char *trim_whitespace(char *str) {
|
|
char *end;
|
|
|
|
// Trim leading whitespace
|
|
while (isspace((unsigned char)*str))
|
|
str++;
|
|
|
|
// All spaces?
|
|
if (*str == 0)
|
|
return str;
|
|
|
|
// Trim trailing whitespace
|
|
end = str + strlen(str) - 1;
|
|
while (end > str && isspace((unsigned char)*end))
|
|
end--;
|
|
|
|
*(end + 1) = '\0';
|
|
return str;
|
|
}
|
|
|
|
// Remove quotes from a string
|
|
static char *remove_quotes(char *str) {
|
|
size_t len = strlen(str);
|
|
if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
|
|
(str[0] == '\'' && str[len - 1] == '\''))) {
|
|
str[len - 1] = '\0';
|
|
return str + 1;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
// Parse boolean value from string
|
|
static bool parse_bool(const char *value) {
|
|
if (!value)
|
|
return false;
|
|
|
|
if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||
|
|
strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// Parse scaling mode from string
|
|
static chroma_scale_mode_t parse_scale_mode(const char *value) {
|
|
if (!value)
|
|
return CHROMA_SCALE_FILL; // default
|
|
|
|
if (strcasecmp(value, "fill") == 0) {
|
|
return CHROMA_SCALE_FILL;
|
|
} else if (strcasecmp(value, "fit") == 0) {
|
|
return CHROMA_SCALE_FIT;
|
|
} else if (strcasecmp(value, "stretch") == 0) {
|
|
return CHROMA_SCALE_STRETCH;
|
|
} else if (strcasecmp(value, "center") == 0) {
|
|
return CHROMA_SCALE_CENTER;
|
|
}
|
|
|
|
chroma_log("WARN", "Unknown scaling mode: %s (using fill)", value);
|
|
return CHROMA_SCALE_FILL;
|
|
}
|
|
|
|
// Parse filter quality from string
|
|
static chroma_filter_quality_t parse_filter_quality(const char *value) {
|
|
if (!value)
|
|
return CHROMA_FILTER_LINEAR; // default
|
|
|
|
if (strcasecmp(value, "nearest") == 0) {
|
|
return CHROMA_FILTER_NEAREST;
|
|
} else if (strcasecmp(value, "linear") == 0) {
|
|
return CHROMA_FILTER_LINEAR;
|
|
} else if (strcasecmp(value, "bilinear") == 0) {
|
|
return CHROMA_FILTER_BILINEAR;
|
|
} else if (strcasecmp(value, "trilinear") == 0) {
|
|
return CHROMA_FILTER_TRILINEAR;
|
|
}
|
|
|
|
chroma_log("WARN", "Unknown filter quality: %s (using linear)", value);
|
|
return CHROMA_FILTER_LINEAR;
|
|
}
|
|
|
|
// Get string representation of scaling mode
|
|
static const char *scale_mode_to_string(chroma_scale_mode_t mode) {
|
|
switch (mode) {
|
|
case CHROMA_SCALE_FILL:
|
|
return "fill";
|
|
case CHROMA_SCALE_FIT:
|
|
return "fit";
|
|
case CHROMA_SCALE_STRETCH:
|
|
return "stretch";
|
|
case CHROMA_SCALE_CENTER:
|
|
return "center";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
// Get string representation of filter quality
|
|
static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
|
|
switch (quality) {
|
|
case CHROMA_FILTER_NEAREST:
|
|
return "nearest";
|
|
case CHROMA_FILTER_LINEAR:
|
|
return "linear";
|
|
case CHROMA_FILTER_BILINEAR:
|
|
return "bilinear";
|
|
case CHROMA_FILTER_TRILINEAR:
|
|
return "trilinear";
|
|
default:
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
// Output-to-image mapping
|
|
static int add_output_mapping(chroma_config_t *config, const char *output_name,
|
|
const char *image_path,
|
|
chroma_scale_mode_t scale_mode,
|
|
chroma_filter_quality_t filter_quality) {
|
|
if (!config || !output_name || !image_path) {
|
|
return CHROMA_ERROR_INIT;
|
|
}
|
|
|
|
if (config->mapping_count >= MAX_OUTPUTS) {
|
|
chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
|
|
MAX_OUTPUTS);
|
|
return CHROMA_ERROR_MEMORY;
|
|
}
|
|
|
|
// Validate string lengths to prevent buffer overflow
|
|
size_t output_len = strlen(output_name);
|
|
size_t path_len = strlen(image_path);
|
|
|
|
if (output_len >= sizeof(config->mappings[0].output_name)) {
|
|
chroma_log("ERROR", "Output name too long: %s (max %zu)", output_name,
|
|
sizeof(config->mappings[0].output_name) - 1);
|
|
return CHROMA_ERROR_CONFIG;
|
|
}
|
|
|
|
if (path_len >= sizeof(config->mappings[0].image_path)) {
|
|
chroma_log("ERROR", "Image path too long: %s (max %zu)", image_path,
|
|
sizeof(config->mappings[0].image_path) - 1);
|
|
return CHROMA_ERROR_CONFIG;
|
|
}
|
|
|
|
chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
|
|
|
|
strcpy(mapping->output_name, output_name);
|
|
strcpy(mapping->image_path, image_path);
|
|
mapping->scale_mode = scale_mode;
|
|
mapping->filter_quality = filter_quality;
|
|
|
|
config->mapping_count++;
|
|
|
|
chroma_log("DEBUG", "Added mapping: %s -> %s (scale: %s, filter: %s)",
|
|
output_name, image_path, scale_mode_to_string(scale_mode),
|
|
filter_quality_to_string(filter_quality));
|
|
chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
|
|
config->mapping_count, output_name, image_path, path_len);
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Initialize configuration with defaults
|
|
static void init_default_config(chroma_config_t *config) {
|
|
if (!config)
|
|
return;
|
|
|
|
memset(config, 0, sizeof(chroma_config_t));
|
|
|
|
config->daemon_mode = false;
|
|
config->mapping_count = 0;
|
|
|
|
// Set default scaling and filtering
|
|
config->default_scale_mode = CHROMA_SCALE_FILL;
|
|
config->default_filter_quality = CHROMA_FILTER_LINEAR;
|
|
|
|
// Set default downsampling settings
|
|
config->enable_downsampling = true; // enable by default, performance etc.
|
|
config->max_output_width = 3840; // 4K width
|
|
config->max_output_height = 2160; // 4K height
|
|
config->min_scale_factor = 0.25f; // don't scale below 25%
|
|
|
|
// Set default image path (can be overridden)
|
|
const char *home = getenv("HOME");
|
|
if (home) {
|
|
snprintf(config->default_image, sizeof(config->default_image),
|
|
"%s/.config/chroma/default.jpg", home);
|
|
} else {
|
|
strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
|
|
}
|
|
}
|
|
|
|
// Parse a single configuration line
|
|
static int parse_config_line(chroma_config_t *config, char *line,
|
|
int line_number) {
|
|
if (!config || !line) {
|
|
return CHROMA_ERROR_INIT;
|
|
}
|
|
|
|
// Skip empty lines and comments
|
|
char *trimmed = trim_whitespace(line);
|
|
if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Find the equals sign
|
|
char *equals = strchr(trimmed, '=');
|
|
if (!equals) {
|
|
chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
|
|
return CHROMA_OK; // continue parsing
|
|
}
|
|
|
|
*equals = '\0';
|
|
char *key = trim_whitespace(trimmed);
|
|
char *value = trim_whitespace(equals + 1);
|
|
|
|
value = remove_quotes(value);
|
|
|
|
if (*key == '\0' || *value == '\0') {
|
|
chroma_log("WARN", "Invalid config line %d: empty key or value",
|
|
line_number);
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Parse configuration options
|
|
if (strcasecmp(key, "default_image") == 0) {
|
|
char *expanded_path = chroma_expand_path(value);
|
|
const char *path_to_use = expanded_path ? expanded_path : value;
|
|
size_t path_len = strlen(path_to_use);
|
|
|
|
if (path_len >= sizeof(config->default_image)) {
|
|
chroma_log("ERROR", "Default image path too long: %s (max %zu)",
|
|
path_to_use, sizeof(config->default_image) - 1);
|
|
if (expanded_path) {
|
|
free(expanded_path);
|
|
}
|
|
return CHROMA_ERROR_CONFIG;
|
|
}
|
|
|
|
strcpy(config->default_image, path_to_use);
|
|
|
|
if (expanded_path) {
|
|
chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
|
|
chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
|
|
path_len, expanded_path);
|
|
free(expanded_path);
|
|
} else {
|
|
chroma_log("WARN", "Failed to expand path, using original: %s", value);
|
|
}
|
|
} else if (strcasecmp(key, "daemon") == 0 ||
|
|
strcasecmp(key, "daemon_mode") == 0) {
|
|
config->daemon_mode = parse_bool(value);
|
|
chroma_log("DEBUG", "Set daemon mode: %s",
|
|
config->daemon_mode ? "true" : "false");
|
|
chroma_log("TRACE",
|
|
"Daemon mode configuration: key='%s', value='%s', parsed=%s",
|
|
key, value, config->daemon_mode ? "true" : "false");
|
|
} else if (strcasecmp(key, "scale_mode") == 0 ||
|
|
strcasecmp(key, "default_scale_mode") == 0) {
|
|
config->default_scale_mode = parse_scale_mode(value);
|
|
chroma_log("DEBUG", "Set default scale mode: %s",
|
|
scale_mode_to_string(config->default_scale_mode));
|
|
chroma_log("TRACE",
|
|
"Scale mode configuration: key='%s', value='%s', parsed=%s", key,
|
|
value, scale_mode_to_string(config->default_scale_mode));
|
|
} else if (strcasecmp(key, "filter_quality") == 0 ||
|
|
strcasecmp(key, "default_filter_quality") == 0) {
|
|
config->default_filter_quality = parse_filter_quality(value);
|
|
chroma_log("DEBUG", "Set default filter quality: %s",
|
|
filter_quality_to_string(config->default_filter_quality));
|
|
chroma_log("TRACE",
|
|
"Filter quality configuration: key='%s', value='%s', parsed=%s",
|
|
key, value,
|
|
filter_quality_to_string(config->default_filter_quality));
|
|
} else if (strcasecmp(key, "enable_downsampling") == 0) {
|
|
config->enable_downsampling = parse_bool(value);
|
|
chroma_log("DEBUG", "Set downsampling: %s",
|
|
config->enable_downsampling ? "enabled" : "disabled");
|
|
} else if (strcasecmp(key, "max_output_width") == 0) {
|
|
int width = atoi(value);
|
|
if (width > 0 && width <= 16384) { // Reasonable limits
|
|
config->max_output_width = width;
|
|
chroma_log("DEBUG", "Set max output width: %d", width);
|
|
} else {
|
|
chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);
|
|
}
|
|
} else if (strcasecmp(key, "max_output_height") == 0) {
|
|
int height = atoi(value);
|
|
if (height > 0 && height <= 16384) { // Reasonable limits
|
|
config->max_output_height = height;
|
|
chroma_log("DEBUG", "Set max output height: %d", height);
|
|
} else {
|
|
chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);
|
|
}
|
|
} else if (strcasecmp(key, "min_scale_factor") == 0) {
|
|
float factor = atof(value);
|
|
if (factor > 0.0f && factor <= 1.0f) { // Valid range
|
|
config->min_scale_factor = factor;
|
|
chroma_log("DEBUG", "Set minimum scale factor: %.2f", factor);
|
|
} else {
|
|
chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
|
|
}
|
|
} else if (strncasecmp(key, "output.", 7) == 0) {
|
|
// Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
|
|
const char *output_name = key + 7;
|
|
if (*output_name == '\0') {
|
|
chroma_log("WARN", "Invalid output mapping line %d: no output name",
|
|
line_number);
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Check for extended output configuration with properties
|
|
// Format: output.DP-1.scale = fill
|
|
// Format: output.DP-1.filter = linear
|
|
char *dot = strchr(output_name, '.');
|
|
if (dot) {
|
|
// This is an output property (scale or filter)
|
|
*dot = '\0';
|
|
const char *property = dot + 1;
|
|
|
|
// Find existing mapping for this output
|
|
chroma_config_mapping_t *mapping = NULL;
|
|
for (int i = 0; i < config->mapping_count; i++) {
|
|
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
|
|
mapping = &config->mappings[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!mapping) {
|
|
chroma_log("WARN", "Output %s not found for property %s (line %d)",
|
|
output_name, property, line_number);
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
if (strcasecmp(property, "scale") == 0) {
|
|
mapping->scale_mode = parse_scale_mode(value);
|
|
chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,
|
|
scale_mode_to_string(mapping->scale_mode));
|
|
} else if (strcasecmp(property, "filter") == 0) {
|
|
mapping->filter_quality = parse_filter_quality(value);
|
|
chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
|
|
filter_quality_to_string(mapping->filter_quality));
|
|
} else {
|
|
chroma_log("WARN", "Unknown output property: %s (line %d)", property,
|
|
line_number);
|
|
}
|
|
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Expand path before validation and storage
|
|
char *expanded_path = chroma_expand_path(value);
|
|
const char *path_to_validate = expanded_path ? expanded_path : value;
|
|
|
|
// Validate image path
|
|
if (chroma_image_validate(path_to_validate) != CHROMA_OK) {
|
|
chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",
|
|
output_name, value, path_to_validate);
|
|
if (expanded_path) {
|
|
free(expanded_path);
|
|
}
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
if (add_output_mapping(config, output_name, path_to_validate,
|
|
config->default_scale_mode,
|
|
config->default_filter_quality) != CHROMA_OK) {
|
|
chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
|
|
path_to_validate);
|
|
if (expanded_path) {
|
|
free(expanded_path);
|
|
}
|
|
return CHROMA_ERROR_CONFIG;
|
|
}
|
|
|
|
if (expanded_path) {
|
|
free(expanded_path);
|
|
}
|
|
} else {
|
|
chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
|
|
key);
|
|
chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
|
|
line_number, key, value);
|
|
}
|
|
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Load configuration from file
|
|
int chroma_config_load(chroma_config_t *config, const char *config_file) {
|
|
if (!config) {
|
|
return CHROMA_ERROR_INIT;
|
|
}
|
|
|
|
// Initialize with defaults
|
|
init_default_config(config);
|
|
|
|
if (!config_file) {
|
|
chroma_log("INFO", "No config file specified, using defaults");
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
FILE *file = fopen(config_file, "r");
|
|
if (!file) {
|
|
if (errno == ENOENT) {
|
|
chroma_log("INFO", "Config file not found: %s (using defaults)",
|
|
config_file);
|
|
return CHROMA_OK;
|
|
} else {
|
|
chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
|
|
strerror(errno));
|
|
return CHROMA_ERROR_CONFIG;
|
|
}
|
|
}
|
|
|
|
chroma_log("INFO", "Loading configuration from: %s", config_file);
|
|
chroma_log("TRACE",
|
|
"Starting configuration parsing, estimated config size: %ld bytes",
|
|
chroma_get_file_size(config_file));
|
|
|
|
char line[1024];
|
|
int line_number = 0;
|
|
int parse_errors = 0;
|
|
|
|
while (fgets(line, sizeof(line), file)) {
|
|
line_number++;
|
|
|
|
char *newline = strchr(line, '\n');
|
|
if (newline) {
|
|
*newline = '\0';
|
|
}
|
|
|
|
if (parse_config_line(config, line, line_number) != CHROMA_OK) {
|
|
parse_errors++;
|
|
}
|
|
}
|
|
|
|
fclose(file);
|
|
|
|
if (parse_errors > 0) {
|
|
chroma_log("WARN", "Config file contained %d errors", parse_errors);
|
|
// Continue anyway with partial configuration
|
|
}
|
|
|
|
chroma_log("INFO",
|
|
"Loaded configuration: %d output mappings, default image: %s",
|
|
config->mapping_count, config->default_image);
|
|
|
|
// Log configuration memory usage
|
|
size_t config_size =
|
|
sizeof(chroma_config_t) +
|
|
(config->mapping_count * sizeof(chroma_config_mapping_t));
|
|
chroma_log_resource_allocation("config_data", config_size,
|
|
"configuration structure");
|
|
chroma_log_memory_stats("post-config-load");
|
|
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
// Free configuration resources
|
|
void chroma_config_free(chroma_config_t *config) {
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
// Log configuration deallocation
|
|
size_t config_size =
|
|
sizeof(chroma_config_t) +
|
|
(config->mapping_count * sizeof(chroma_config_mapping_t));
|
|
chroma_log_resource_deallocation("config_data", config_size,
|
|
"configuration structure");
|
|
|
|
chroma_log("TRACE", "Clearing %d output mappings from configuration",
|
|
config->mapping_count);
|
|
memset(config->mappings, 0, sizeof(config->mappings));
|
|
config->mapping_count = 0;
|
|
|
|
memset(config->default_image, 0, sizeof(config->default_image));
|
|
|
|
chroma_log("DEBUG", "Freed configuration");
|
|
}
|
|
|
|
// Get image path for specific output
|
|
const char *chroma_config_get_image_for_output(chroma_config_t *config,
|
|
const char *output_name) {
|
|
if (!config || !output_name) {
|
|
return NULL;
|
|
}
|
|
|
|
// Look for specific output mapping
|
|
for (int i = 0; i < config->mapping_count; i++) {
|
|
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
|
|
chroma_log("DEBUG", "Found specific mapping for output %s: %s",
|
|
output_name, config->mappings[i].image_path);
|
|
return config->mappings[i].image_path;
|
|
}
|
|
}
|
|
|
|
// Return default image if no specific mapping found
|
|
if (strlen(config->default_image) > 0) {
|
|
chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
|
|
config->default_image);
|
|
return config->default_image;
|
|
}
|
|
|
|
chroma_log("WARN", "No image configured for output: %s", output_name);
|
|
return NULL;
|
|
}
|
|
|
|
// Get configuration mapping for output, including scale mode and filter
|
|
// quality
|
|
int chroma_config_get_mapping_for_output(
|
|
chroma_config_t *config, const char *output_name,
|
|
chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality) {
|
|
if (!config || !output_name || !scale_mode || !filter_quality) {
|
|
return CHROMA_ERROR_INIT;
|
|
}
|
|
|
|
// Look for specific output mapping
|
|
for (int i = 0; i < config->mapping_count; i++) {
|
|
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
|
|
*scale_mode = config->mappings[i].scale_mode;
|
|
*filter_quality = config->mappings[i].filter_quality;
|
|
chroma_log("DEBUG",
|
|
"Found specific mapping for output %s: scale=%s, filter=%s",
|
|
output_name, scale_mode_to_string(*scale_mode),
|
|
filter_quality_to_string(*filter_quality));
|
|
return CHROMA_OK;
|
|
}
|
|
}
|
|
|
|
// Return defaults if no specific mapping found
|
|
*scale_mode = config->default_scale_mode;
|
|
*filter_quality = config->default_filter_quality;
|
|
chroma_log("DEBUG", "Using defaults for output %s: scale=%s, filter=%s",
|
|
output_name, scale_mode_to_string(*scale_mode),
|
|
filter_quality_to_string(*filter_quality));
|
|
return CHROMA_OK;
|
|
}
|
|
|
|
|
|
|
|
// Print current configuration for debugging
|
|
void chroma_config_print(const chroma_config_t *config) {
|
|
if (!config) {
|
|
return;
|
|
}
|
|
|
|
chroma_log("INFO", "=== Configuration ===");
|
|
chroma_log("INFO", "Default image: %s", config->default_image);
|
|
chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
|
|
chroma_log("INFO", "Default scale mode: %s",
|
|
scale_mode_to_string(config->default_scale_mode));
|
|
chroma_log("INFO", "Default filter quality: %s",
|
|
filter_quality_to_string(config->default_filter_quality));
|
|
chroma_log("INFO", "Downsampling: %s",
|
|
config->enable_downsampling ? "enabled" : "disabled");
|
|
if (config->enable_downsampling) {
|
|
chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
|
|
config->max_output_height);
|
|
chroma_log("INFO", "Min scale factor: %.2f", config->min_scale_factor);
|
|
}
|
|
chroma_log("INFO", "Output mappings: %d", config->mapping_count);
|
|
|
|
for (int i = 0; i < config->mapping_count; i++) {
|
|
chroma_log("INFO", " %s -> %s (scale: %s, filter: %s)",
|
|
config->mappings[i].output_name, config->mappings[i].image_path,
|
|
scale_mode_to_string(config->mappings[i].scale_mode),
|
|
filter_quality_to_string(config->mappings[i].filter_quality));
|
|
chroma_log(
|
|
"TRACE",
|
|
" Mapping %d: output='%s', image='%s', scale='%s', filter='%s', "
|
|
"path_exists=%s",
|
|
i, config->mappings[i].output_name, config->mappings[i].image_path,
|
|
scale_mode_to_string(config->mappings[i].scale_mode),
|
|
filter_quality_to_string(config->mappings[i].filter_quality),
|
|
chroma_path_exists(config->mappings[i].image_path) ? "yes" : "no");
|
|
}
|
|
chroma_log("INFO", "====================");
|
|
}
|