diff --git a/.gitignore b/.gitignore index 339fad0..5e4f977 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ src/wlr-layer-shell-unstable-v1.c # Ignore test stuff that I create to... test stuff. test_* +*.jpg +*.conf diff --git a/Makefile b/Makefile index 64aaaf8..2d08a64 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ PROJECT_NAME = chroma -VERSION = 1.0.0 +VERSION = 1.0.1 # Directories SRCDIR = src @@ -69,7 +69,7 @@ $(INCDIR): @mkdir -p $(INCDIR) # Build main executable -$(TARGET): $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR) +$(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR) @echo " LINK $@" @$(CC) $(OBJECTS) -o $@ $(LDFLAGS) @@ -110,6 +110,16 @@ uninstall: rm -f $(DESTDIR)$(SYSTEMD_INSTALL)/$(PROJECT_NAME).service @echo "Uninstall complete." +# Create version header +version-header: + @echo "Generating version header..." + @echo "#ifndef CHROMA_VERSION_H" > $(INCDIR)/chroma_version.h + @echo "#define CHROMA_VERSION_H" >> $(INCDIR)/chroma_version.h + @echo "" >> $(INCDIR)/chroma_version.h + @echo "#define CHROMA_VERSION \"$(VERSION)\"" >> $(INCDIR)/chroma_version.h + @echo "" >> $(INCDIR)/chroma_version.h + @echo "#endif // CHROMA_VERSION_H" >> $(INCDIR)/chroma_version.h + # Create systemd service file systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service @@ -140,33 +150,68 @@ test: $(TARGET) @echo "Running tests..." @echo "Tests not implemented yet." +# Version management targets +bump-patch: + @echo "Bumping patch version..." + @$(eval NEW_VERSION := $(shell echo $(VERSION) | awk -F. '{print $$1"."$$2"."$$3+1}')) + @sed -i 's/^VERSION = .*/VERSION = $(NEW_VERSION)/' Makefile + @echo "Version bumped to $(NEW_VERSION)" + +bump-minor: + @echo "Bumping minor version..." + @$(eval NEW_VERSION := $(shell echo $(VERSION) | awk -F. '{print $$1"."$$2+1".0"}')) + @sed -i 's/^VERSION = .*/VERSION = $(NEW_VERSION)/' Makefile + @echo "Version bumped to $(NEW_VERSION)" + +bump-major: + @echo "Bumping major version..." + @$(eval NEW_VERSION := $(shell echo $(VERSION) | awk -F. '{print $$1+1".0.0"}')) + @sed -i 's/^VERSION = .*/VERSION = $(NEW_VERSION)/' Makefile + @echo "Version bumped to $(NEW_VERSION)" + +set-version: + @if [ -z "$(NEW_VER)" ]; then \ + echo "Usage: make set-version NEW_VER=X.Y.Z"; \ + exit 1; \ + fi + @echo "Setting version to $(NEW_VER)..." + @sed -i 's/^VERSION = .*/VERSION = $(NEW_VER)/' Makefile + @echo "Version set to $(NEW_VER)" + # Help target help: @echo "Available targets:" - @echo " all - Build the main executable (default)" + @echo " all - Build main executable (default)" @echo " debug - Build with debug symbols and sanitizers" @echo " static - Build statically linked executable" @echo " check-deps - Check if all dependencies are available" - @echo " install - Install the executable and systemd service" + @echo " install - Install executable and systemd service" @echo " uninstall - Remove installed files" @echo " clean - Remove build artifacts" @echo " distclean - Remove all generated files" @echo " format - Format source code (requires clang-format)" @echo " analyze - Run static analysis (requires cppcheck)" @echo " test - Run tests" + @echo " version-header - Generate version header from Makefile" + @echo " bump-patch - Increment patch version (X.Y.Z+1)" + @echo " bump-minor - Increment minor version (X.Y+1.0)" + @echo " bump-major - Increment major version (X+1.0.0)" + @echo " set-version - Set specific version (use NEW_VER=X.Y.Z)" @echo " help - Show this help message" @echo "" @echo "Examples:" - @echo " make # Build with default settings" - @echo " make debug # Build debug version" - @echo " make PREFIX=/usr install # Install to /usr instead of /usr/local" - @echo " make CC=clang # Use clang instead of gcc" + @echo " make # Build with default settings" + @echo " make debug # Build debug version" + @echo " make PREFIX=/usr install # Install to /usr instead of /usr/local" + @echo " make CC=clang # Use clang instead of gcc" + @echo " make bump-patch # Bump to 1.0.2" + @echo " make set-version NEW_VER=2.0.0 # Set to 2.0.0" # Include dependency files -include $(DEPENDS) # Phony targets -.PHONY: all debug static check-deps install uninstall systemd-service sample-config clean distclean format analyze test help +.PHONY: all debug static check-deps install uninstall systemd-service version-header sample-config clean distclean format analyze test help bump-patch bump-minor bump-major set-version # Print variables print-%: diff --git a/include/chroma.h b/include/chroma.h index 397041f..eee298a 100644 --- a/include/chroma.h +++ b/include/chroma.h @@ -12,7 +12,8 @@ #include #include -#define CHROMA_VERSION "1.0.0" +#include "chroma_version.h" + #define MAX_OUTPUTS 16 #define MAX_PATH_LEN 4096 #define CONFIG_FILE_NAME "chroma.conf" @@ -35,6 +36,22 @@ typedef enum { CHROMA_ERROR_MEMORY = -6 } chroma_error_t; +// Scaling modes for wallpaper display +typedef enum { + CHROMA_SCALE_FILL = 0, // fill entire output, crop if necessary + CHROMA_SCALE_FIT = 1, // fit image within output, add borders if needed + CHROMA_SCALE_STRETCH = 2, // stretch to fill output, may distort aspect ratio + CHROMA_SCALE_CENTER = 3 // center image at original size +} chroma_scale_mode_t; + +// Image filtering quality settings +typedef enum { + CHROMA_FILTER_NEAREST = 0, // nearest neighbor filtering (pixelated) + CHROMA_FILTER_LINEAR = 1, // linear filtering (smooth) + CHROMA_FILTER_BILINEAR = 2, // bilinear filtering (smoother) + CHROMA_FILTER_TRILINEAR = 3 // trilinear filtering (smoothest) +} chroma_filter_quality_t; + // Image data structure typedef struct { unsigned char *data; // RGBA pixel data @@ -70,6 +87,11 @@ typedef struct { // Associated wallpaper chroma_image_t *image; + // Configuration for this output + chroma_scale_mode_t scale_mode; + chroma_filter_quality_t filter_quality; + bool config_loaded; + // OpenGL resource cache GLuint texture_id; GLuint shader_program; @@ -83,6 +105,8 @@ typedef struct { typedef struct { char output_name[256]; char image_path[MAX_PATH_LEN]; + chroma_scale_mode_t scale_mode; + chroma_filter_quality_t filter_quality; } chroma_config_mapping_t; // Application configuration @@ -91,6 +115,10 @@ typedef struct { int mapping_count; char default_image[MAX_PATH_LEN]; bool daemon_mode; + + // Global scaling and filtering settings (used as defaults) + chroma_scale_mode_t default_scale_mode; + chroma_filter_quality_t default_filter_quality; } chroma_config_t; // Main application state @@ -193,6 +221,11 @@ int chroma_config_load(chroma_config_t *config, const char *config_file); void chroma_config_free(chroma_config_t *config); const char *chroma_config_get_image_for_output(chroma_config_t *config, const char *output_name); +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); +int chroma_config_create_sample(const char *config_file); +void chroma_config_print(const chroma_config_t *config); // Main loop and events int chroma_run(chroma_state_t *state); diff --git a/include/chroma_version.h b/include/chroma_version.h new file mode 100644 index 0000000..1ab736b --- /dev/null +++ b/include/chroma_version.h @@ -0,0 +1,8 @@ +#ifndef CHROMA_VERSION_H +#define CHROMA_VERSION_H + +#ifndef CHROMA_VERSION +#define CHROMA_VERSION "1.0.1" +#endif + +#endif // CHROMA_VERSION_H diff --git a/src/config.c b/src/config.c index 5fcbc9e..6dde3b1 100644 --- a/src/config.c +++ b/src/config.c @@ -52,11 +52,81 @@ static bool parse_bool(const char *value) { return false; } -// Parse integer value from string +// 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) { + 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; } @@ -67,20 +137,36 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name, 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]; - strncpy(mapping->output_name, output_name, sizeof(mapping->output_name) - 1); - mapping->output_name[sizeof(mapping->output_name) - 1] = '\0'; - - strncpy(mapping->image_path, image_path, sizeof(mapping->image_path) - 1); - mapping->image_path[sizeof(mapping->image_path) - 1] = '\0'; + 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", output_name, image_path); + 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, - strlen(image_path)); + config->mapping_count, output_name, image_path, path_len); return CHROMA_OK; } @@ -94,6 +180,10 @@ static void init_default_config(chroma_config_t *config) { 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 image path (can be overridden) const char *home = getenv("HOME"); if (home) { @@ -138,11 +228,29 @@ static int parse_config_line(chroma_config_t *config, char *line, // Parse configuration options if (strcasecmp(key, "default_image") == 0) { - strncpy(config->default_image, value, sizeof(config->default_image) - 1); - config->default_image[sizeof(config->default_image) - 1] = '\0'; - chroma_log("DEBUG", "Set default image: %s", value); - chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'", - strlen(value), value); + 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); @@ -151,6 +259,23 @@ static int parse_config_line(chroma_config_t *config, char *line, 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 (strncasecmp(key, "output.", 7) == 0) { // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg const char *output_name = key + 7; @@ -160,18 +285,74 @@ static int parse_config_line(chroma_config_t *config, char *line, return CHROMA_OK; } - // Validate image path - if (chroma_image_validate(value) != CHROMA_OK) { - chroma_log("WARN", "Invalid image path for output %s: %s", output_name, - value); + // 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; } - if (add_output_mapping(config, output_name, value) != 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, - value); + 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); @@ -303,6 +484,37 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config, 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; +} + // Create a sample configuration file int chroma_config_create_sample(const char *config_file) { if (!config_file) { @@ -346,14 +558,24 @@ void chroma_config_print(const chroma_config_t *config) { 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", "Output mappings: %d", config->mapping_count); for (int i = 0; i < config->mapping_count; i++) { - chroma_log("INFO", " %s -> %s", config->mappings[i].output_name, - config->mappings[i].image_path); + 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', path_exists=%s", i, - config->mappings[i].output_name, config->mappings[i].image_path, + "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", "===================="); diff --git a/src/core.c b/src/core.c index 8a13255..63d904d 100644 --- a/src/core.c +++ b/src/core.c @@ -103,6 +103,32 @@ static int assign_wallpaper_to_output(chroma_state_t *state, // Assign image to output output->image = image; + // Store old configuration values for comparison + chroma_scale_mode_t old_scale_mode = output->scale_mode; + chroma_filter_quality_t old_filter_quality = output->filter_quality; + bool had_config = output->config_loaded; + + // Load configuration for this output (scale mode and filter quality) + if (chroma_config_get_mapping_for_output( + &state->config, output->name ? output->name : "unknown", + &output->scale_mode, &output->filter_quality) == CHROMA_OK) { + output->config_loaded = true; + chroma_log("DEBUG", "Loaded config for output %u: scale=%d, filter=%d", + output->id, output->scale_mode, output->filter_quality); + + // Check if configuration changed and invalidate texture if needed + if (had_config && (old_scale_mode != output->scale_mode || + old_filter_quality != output->filter_quality)) { + chroma_output_invalidate_texture(output); + chroma_log("DEBUG", + "Configuration changed for output %u, invalidated texture", + output->id); + } + } else { + output->config_loaded = false; + chroma_log("WARN", "Failed to load config for output %u", output->id); + } + // Create surface if it doesn't exist if (!output->surface) { int ret = chroma_surface_create(state, output); diff --git a/src/image.c b/src/image.c index 53e03d8..6a22991 100644 --- a/src/image.c +++ b/src/image.c @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "../include/chroma.h" diff --git a/src/render.c b/src/render.c index b73e1ab..be9f98a 100644 --- a/src/render.c +++ b/src/render.c @@ -8,6 +8,146 @@ #include "../include/chroma.h" #include "../include/stb_image.h" +// Convert filter quality enum to OpenGL parameters +static void get_gl_filter_params(chroma_filter_quality_t quality, + GLint *min_filter, GLint *mag_filter) { + switch (quality) { + case CHROMA_FILTER_NEAREST: + *min_filter = GL_NEAREST; + *mag_filter = GL_NEAREST; + break; + case CHROMA_FILTER_LINEAR: + *min_filter = GL_LINEAR; + *mag_filter = GL_LINEAR; + break; + case CHROMA_FILTER_BILINEAR: + *min_filter = GL_LINEAR_MIPMAP_LINEAR; + *mag_filter = GL_LINEAR; + break; + case CHROMA_FILTER_TRILINEAR: + *min_filter = GL_LINEAR_MIPMAP_LINEAR; + *mag_filter = GL_LINEAR; + break; + default: + *min_filter = GL_LINEAR; + *mag_filter = GL_LINEAR; + break; + } +} + +// Calculate texture coordinates based on scaling mode +static void calculate_texture_coords(chroma_scale_mode_t scale_mode, + int image_width, int image_height, + int output_width, int output_height, + float tex_coords[8]) { + // Default texture coordinates (full texture) + float u1 = 0.0f, v1 = 0.0f; // top-left + float u2 = 1.0f, v2 = 1.0f; // bottom-right + + switch (scale_mode) { + case CHROMA_SCALE_STRETCH: + // Use full texture, stretch to fit + u1 = 0.0f; + v1 = 0.0f; + u2 = 1.0f; + v2 = 1.0f; + break; + + case CHROMA_SCALE_CENTER: + // Center image at original size + // Calculate how much of the texture to show + { + float image_aspect = (float)image_width / image_height; + float output_aspect = (float)output_width / output_height; + + if (image_aspect > output_aspect) { + // Image is wider - fit width, show center portion vertically + float visible_height = (float)image_width / output_aspect; + float v_offset = + (image_height - visible_height) / (2.0f * image_height); + u1 = 0.0f; + v1 = v_offset; + u2 = 1.0f; + v2 = 1.0f - v_offset; + } else { + // Image is taller - fit height, show center portion horizontally + float visible_width = (float)image_height * output_aspect; + float u_offset = (image_width - visible_width) / (2.0f * image_width); + u1 = u_offset; + v1 = 0.0f; + u2 = 1.0f - u_offset; + v2 = 1.0f; + } + } + break; + + case CHROMA_SCALE_FIT: + // Fit image within output, maintaining aspect ratio + { + float image_aspect = (float)image_width / image_height; + float output_aspect = (float)output_width / output_height; + + if (image_aspect > output_aspect) { + // Image is wider - fit width, add borders top/bottom + float scaled_height = (float)output_width / image_aspect; + float v_border = + (output_height - scaled_height) / (2.0f * output_height); + u1 = 0.0f; + v1 = v_border; + u2 = 1.0f; + v2 = 1.0f - v_border; + } else { + // Image is taller - fit height, add borders left/right + float scaled_width = (float)output_height * image_aspect; + float u_border = (output_width - scaled_width) / (2.0f * output_width); + u1 = u_border; + v1 = 0.0f; + u2 = 1.0f - u_border; + v2 = 1.0f; + } + } + break; + + case CHROMA_SCALE_FILL: + default: + // Fill entire output, crop if necessary + { + float image_aspect = (float)image_width / image_height; + float output_aspect = (float)output_width / output_height; + + if (image_aspect > output_aspect) { + // Image is wider - crop left/right + float crop_width = image_height * output_aspect; + float u_crop = (image_width - crop_width) / (2.0f * image_width); + u1 = u_crop; + v1 = 0.0f; + u2 = 1.0f - u_crop; + v2 = 1.0f; + } else { + // Image is taller - crop top/bottom + float crop_height = image_width / output_aspect; + float v_crop = (image_height - crop_height) / (2.0f * image_height); + u1 = 0.0f; + v1 = v_crop; + u2 = 1.0f; + v2 = 1.0f - v_crop; + } + } + break; + } + + // Set texture coordinates for quad (bottom-left, bottom-right, top-right, + // top-left) + tex_coords[0] = u1; + tex_coords[1] = v2; // bottom-left + tex_coords[2] = u2; + tex_coords[3] = v2; // bottom-right + tex_coords[4] = u2; + tex_coords[5] = v1; // top-right + tex_coords[6] = u1; + tex_coords[7] = v1; // top-left +} + // Vertex shader for simple texture rendering static const char *vertex_shader_source = "#version 120\n" @@ -125,7 +265,7 @@ static int init_gl_resources(chroma_output_t *output) { glGenBuffers(1, &output->ebo); glBindBuffer(GL_ARRAY_BUFFER, output->vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_DYNAMIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, @@ -140,7 +280,8 @@ static int init_gl_resources(chroma_output_t *output) { // Create or update texture from image data static int update_texture_from_image(chroma_output_t *output, - chroma_image_t *image) { + chroma_image_t *image, + chroma_filter_quality_t filter_quality) { if (!output || !image || !image->loaded) { return CHROMA_ERROR_INIT; } @@ -180,8 +321,12 @@ static int update_texture_from_image(chroma_output_t *output, // Set texture parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Use configured filter quality + GLint min_filter, mag_filter; + get_gl_filter_params(filter_quality, &min_filter, &mag_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); // Upload texture data (always RGBA now) glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, @@ -503,7 +648,8 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { } if (output->texture_id == 0) { - if (update_texture_from_image(output, output->image) != CHROMA_OK) { + if (update_texture_from_image(output, output->image, + output->filter_quality) != CHROMA_OK) { chroma_log("ERROR", "Failed to update texture for output %u", output->id); return CHROMA_ERROR_EGL; } @@ -524,8 +670,25 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { glBindTexture(GL_TEXTURE_2D, output->texture_id); glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0); - // Use cached VBO/EBO + // Calculate texture coordinates based on scaling mode + float tex_coords[8]; + calculate_texture_coords(output->scale_mode, output->image->width, + output->image->height, output->width, output->height, + tex_coords); + + // Create dynamic vertex data with calculated texture coordinates + float dynamic_vertices[] = { + // Position Texcoord + -1.0f, -1.0f, tex_coords[0], tex_coords[1], // bottom-left + 1.0f, -1.0f, tex_coords[2], tex_coords[3], // bottom-right + 1.0f, 1.0f, tex_coords[4], tex_coords[5], // top-right + -1.0f, 1.0f, tex_coords[6], tex_coords[7] // top-left + }; + + // Update VBO with dynamic texture coordinates glBindBuffer(GL_ARRAY_BUFFER, output->vbo); + glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices), + dynamic_vertices); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); // Set vertex attributes diff --git a/src/utils.c b/src/utils.c index 8b66fd3..5341c5b 100644 --- a/src/utils.c +++ b/src/utils.c @@ -94,18 +94,138 @@ void chroma_cleanup_signals(void) { g_config_file = NULL; } -// Expand tilde in path +// Expand environment variables in a string +static char *expand_env_vars(const char *str) { + if (!str || strchr(str, '$') == NULL) { + return strdup(str); + } + + char *result = strdup(""); + if (!result) { + return NULL; + } + + const char *p = str; + while (*p) { + if (*p == '$') { + p++; + if (*p == '{') { + // ${VAR} format + p++; + const char *end = strchr(p, '}'); + if (!end) { + // No closing brace, treat as literal + char *tmp = realloc(result, strlen(result) + 2); + if (!tmp) { + free(result); + return NULL; + } + result = tmp; + strcat(result, "${"); + break; + } + + size_t var_len = end - p; + char *var_name = malloc(var_len + 1); + if (!var_name) { + free(result); + return NULL; + } + strncpy(var_name, p, var_len); + var_name[var_len] = '\0'; + + const char *var_value = getenv(var_name); + if (var_value) { + char *tmp = realloc(result, strlen(result) + strlen(var_value) + 1); + if (!tmp) { + free(var_name); + free(result); + return NULL; + } + result = tmp; + strcat(result, var_value); + } + + free(var_name); + p = end + 1; + } else { + // $VAR format + const char *start = p; + while (*p && (isalnum((unsigned char)*p) || *p == '_')) { + p++; + } + + if (p == start) { + // Not a valid variable name, treat $ as literal + char *tmp = realloc(result, strlen(result) + 2); + if (!tmp) { + free(result); + return NULL; + } + result = tmp; + strcat(result, "$"); + } else { + size_t var_len = p - start; + char *var_name = malloc(var_len + 1); + if (!var_name) { + free(result); + return NULL; + } + strncpy(var_name, start, var_len); + var_name[var_len] = '\0'; + + const char *var_value = getenv(var_name); + if (var_value) { + char *tmp = realloc(result, strlen(result) + strlen(var_value) + 1); + if (!tmp) { + free(var_name); + free(result); + return NULL; + } + result = tmp; + strcat(result, var_value); + } + + free(var_name); + } + } + } else { + // Regular character + size_t len = strlen(result); + char *tmp = realloc(result, len + 2); + if (!tmp) { + free(result); + return NULL; + } + result = tmp; + result[len] = *p; + result[len + 1] = '\0'; + p++; + } + } + + return result; +} + +// Expand tilde and environment variables in path char *chroma_expand_path(const char *path) { if (!path) { return NULL; } - if (path[0] != '~') { - return strdup(path); + // First expand environment variables + char *env_expanded = expand_env_vars(path); + if (!env_expanded) { + return NULL; + } + + // Then expand tilde if present + if (env_expanded[0] != '~') { + return env_expanded; } const char *home; - if (path[1] == '/' || path[1] == '\0') { + if (env_expanded[1] == '/' || env_expanded[1] == '\0') { // ~/... or just ~ home = getenv("HOME"); if (!home) { @@ -116,34 +236,39 @@ char *chroma_expand_path(const char *path) { } if (!home) { chroma_log("ERROR", "Could not determine home directory"); + free(env_expanded); return strdup(path); // Return original path as fallback } size_t home_len = strlen(home); - size_t path_len = strlen(path); + size_t path_len = strlen(env_expanded); char *expanded = malloc(home_len + path_len); // -1 for ~ +1 for \0 if (!expanded) { chroma_log("ERROR", "Failed to allocate memory for path expansion"); + free(env_expanded); return strdup(path); } strcpy(expanded, home); - if (path[1] == '/') { - strcat(expanded, path + 1); + if (env_expanded[1] == '/') { + strcat(expanded, env_expanded + 1); } + free(env_expanded); return expanded; } else { // ~user/... - const char *slash = strchr(path, '/'); - size_t user_len = slash ? (size_t)(slash - path - 1) : strlen(path) - 1; + const char *slash = strchr(env_expanded, '/'); + size_t user_len = + slash ? (size_t)(slash - env_expanded - 1) : strlen(env_expanded) - 1; char *username = malloc(user_len + 1); if (!username) { + free(env_expanded); return strdup(path); } - strncpy(username, path + 1, user_len); + strncpy(username, env_expanded + 1, user_len); username[user_len] = '\0'; struct passwd *pw = getpwnam(username); @@ -151,6 +276,7 @@ char *chroma_expand_path(const char *path) { if (!pw) { chroma_log("ERROR", "User not found: %s", username); free(username); + free(env_expanded); return strdup(path); } @@ -160,6 +286,7 @@ char *chroma_expand_path(const char *path) { size_t remaining_len = slash ? strlen(slash) : 0; char *expanded = malloc(home_len + remaining_len + 1); if (!expanded) { + free(env_expanded); return strdup(path); } @@ -168,6 +295,7 @@ char *chroma_expand_path(const char *path) { strcat(expanded, slash); } + free(env_expanded); return expanded; } } @@ -413,4 +541,4 @@ void chroma_format_memory_size(size_t bytes, char *buffer, size_t buffer_size) { } // Cleanup utility functions -void chroma_utils_cleanup(void) { chroma_cleanup_signals(); } \ No newline at end of file +void chroma_utils_cleanup(void) { chroma_cleanup_signals(); }