Compare commits

...

7 commits

Author SHA1 Message Date
74f46f45bf
chroma: add scaling modes to header
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8935a2a5a8e5e33d38e937502b3b82456a6a6964
2025-11-02 01:19:59 +03:00
5fd2e5660f
config: handle transform opts; make implementation futureproof
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic67d4485d08114f605a6dc2535224b276a6a6964
2025-11-02 01:19:58 +03:00
1a366d2445
core: store old config values per-monitor
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ieb3a272935892c874339b13e63b998fd6a6a6964
2025-11-02 01:19:57 +03:00
e7f107a8fe
render: calculate texture coordinates based on scaling mode
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie4e44a0cea68cbbee8122576c41aa4486a6a6964
2025-11-02 01:19:56 +03:00
9b42e70054
chore: bump version; add version management targets
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6e48eced0a604c9d3bd6f4f317f4d5236a6a6964
2025-11-02 01:19:55 +03:00
3d42f75052
meta: ignore more test stuff
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1d79aa394ceac5746d13eba4f7dbb50c6a6a6964
2025-11-02 01:19:54 +03:00
74fed80a26
config: fix tilde expansion for wallpaper paths
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I45b7a520f1959886793ded62f1ed2cd96a6a6964
2025-11-02 01:19:53 +03:00
9 changed files with 680 additions and 52 deletions

2
.gitignore vendored
View file

@ -9,3 +9,5 @@ src/wlr-layer-shell-unstable-v1.c
# Ignore test stuff that I create to... test stuff. # Ignore test stuff that I create to... test stuff.
test_* test_*
*.jpg
*.conf

View file

@ -1,5 +1,5 @@
PROJECT_NAME = chroma PROJECT_NAME = chroma
VERSION = 1.0.0 VERSION = 1.0.1
# Directories # Directories
SRCDIR = src SRCDIR = src
@ -69,7 +69,7 @@ $(INCDIR):
@mkdir -p $(INCDIR) @mkdir -p $(INCDIR)
# Build main executable # Build main executable
$(TARGET): $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR) $(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
@echo " LINK $@" @echo " LINK $@"
@$(CC) $(OBJECTS) -o $@ $(LDFLAGS) @$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
@ -110,6 +110,16 @@ uninstall:
rm -f $(DESTDIR)$(SYSTEMD_INSTALL)/$(PROJECT_NAME).service rm -f $(DESTDIR)$(SYSTEMD_INSTALL)/$(PROJECT_NAME).service
@echo "Uninstall complete." @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 # Create systemd service file
systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service
@ -140,33 +150,68 @@ test: $(TARGET)
@echo "Running tests..." @echo "Running tests..."
@echo "Tests not implemented yet." @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 target
help: help:
@echo "Available targets:" @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 " debug - Build with debug symbols and sanitizers"
@echo " static - Build statically linked executable" @echo " static - Build statically linked executable"
@echo " check-deps - Check if all dependencies are available" @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 " uninstall - Remove installed files"
@echo " clean - Remove build artifacts" @echo " clean - Remove build artifacts"
@echo " distclean - Remove all generated files" @echo " distclean - Remove all generated files"
@echo " format - Format source code (requires clang-format)" @echo " format - Format source code (requires clang-format)"
@echo " analyze - Run static analysis (requires cppcheck)" @echo " analyze - Run static analysis (requires cppcheck)"
@echo " test - Run tests" @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 " help - Show this help message"
@echo "" @echo ""
@echo "Examples:" @echo "Examples:"
@echo " make # Build with default settings" @echo " make # Build with default settings"
@echo " make debug # Build debug version" @echo " make debug # Build debug version"
@echo " make PREFIX=/usr install # Install to /usr instead of /usr/local" @echo " make PREFIX=/usr install # Install to /usr instead of /usr/local"
@echo " make CC=clang # Use clang instead of gcc" @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 dependency files
-include $(DEPENDS) -include $(DEPENDS)
# Phony targets # 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 variables
print-%: print-%:

35
include/chroma.h vendored
View file

@ -12,7 +12,8 @@
#include <wayland-client.h> #include <wayland-client.h>
#include <wayland-egl.h> #include <wayland-egl.h>
#define CHROMA_VERSION "1.0.0" #include "chroma_version.h"
#define MAX_OUTPUTS 16 #define MAX_OUTPUTS 16
#define MAX_PATH_LEN 4096 #define MAX_PATH_LEN 4096
#define CONFIG_FILE_NAME "chroma.conf" #define CONFIG_FILE_NAME "chroma.conf"
@ -35,6 +36,22 @@ typedef enum {
CHROMA_ERROR_MEMORY = -6 CHROMA_ERROR_MEMORY = -6
} chroma_error_t; } 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 // Image data structure
typedef struct { typedef struct {
unsigned char *data; // RGBA pixel data unsigned char *data; // RGBA pixel data
@ -70,6 +87,11 @@ typedef struct {
// Associated wallpaper // Associated wallpaper
chroma_image_t *image; 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 // OpenGL resource cache
GLuint texture_id; GLuint texture_id;
GLuint shader_program; GLuint shader_program;
@ -83,6 +105,8 @@ typedef struct {
typedef struct { typedef struct {
char output_name[256]; char output_name[256];
char image_path[MAX_PATH_LEN]; char image_path[MAX_PATH_LEN];
chroma_scale_mode_t scale_mode;
chroma_filter_quality_t filter_quality;
} chroma_config_mapping_t; } chroma_config_mapping_t;
// Application configuration // Application configuration
@ -91,6 +115,10 @@ typedef struct {
int mapping_count; int mapping_count;
char default_image[MAX_PATH_LEN]; char default_image[MAX_PATH_LEN];
bool daemon_mode; 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; } chroma_config_t;
// Main application state // 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); void chroma_config_free(chroma_config_t *config);
const char *chroma_config_get_image_for_output(chroma_config_t *config, const char *chroma_config_get_image_for_output(chroma_config_t *config,
const char *output_name); 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 // Main loop and events
int chroma_run(chroma_state_t *state); int chroma_run(chroma_state_t *state);

8
include/chroma_version.h Normal file
View file

@ -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

View file

@ -52,11 +52,81 @@ static bool parse_bool(const char *value) {
return false; 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 // Output-to-image mapping
static int add_output_mapping(chroma_config_t *config, const char *output_name, 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) { if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT; 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; 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]; chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
strncpy(mapping->output_name, output_name, sizeof(mapping->output_name) - 1); strcpy(mapping->output_name, output_name);
mapping->output_name[sizeof(mapping->output_name) - 1] = '\0'; strcpy(mapping->image_path, image_path);
mapping->scale_mode = scale_mode;
strncpy(mapping->image_path, image_path, sizeof(mapping->image_path) - 1); mapping->filter_quality = filter_quality;
mapping->image_path[sizeof(mapping->image_path) - 1] = '\0';
config->mapping_count++; 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)", chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
config->mapping_count, output_name, image_path, config->mapping_count, output_name, image_path, path_len);
strlen(image_path));
return CHROMA_OK; return CHROMA_OK;
} }
@ -94,6 +180,10 @@ static void init_default_config(chroma_config_t *config) {
config->daemon_mode = false; config->daemon_mode = false;
config->mapping_count = 0; 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) // Set default image path (can be overridden)
const char *home = getenv("HOME"); const char *home = getenv("HOME");
if (home) { if (home) {
@ -138,11 +228,29 @@ static int parse_config_line(chroma_config_t *config, char *line,
// Parse configuration options // Parse configuration options
if (strcasecmp(key, "default_image") == 0) { if (strcasecmp(key, "default_image") == 0) {
strncpy(config->default_image, value, sizeof(config->default_image) - 1); char *expanded_path = chroma_expand_path(value);
config->default_image[sizeof(config->default_image) - 1] = '\0'; const char *path_to_use = expanded_path ? expanded_path : value;
chroma_log("DEBUG", "Set default image: %s", value); size_t path_len = strlen(path_to_use);
chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
strlen(value), value); 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 || } else if (strcasecmp(key, "daemon") == 0 ||
strcasecmp(key, "daemon_mode") == 0) { strcasecmp(key, "daemon_mode") == 0) {
config->daemon_mode = parse_bool(value); config->daemon_mode = parse_bool(value);
@ -151,6 +259,23 @@ static int parse_config_line(chroma_config_t *config, char *line,
chroma_log("TRACE", chroma_log("TRACE",
"Daemon mode configuration: key='%s', value='%s', parsed=%s", "Daemon mode configuration: key='%s', value='%s', parsed=%s",
key, value, config->daemon_mode ? "true" : "false"); 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) { } else if (strncasecmp(key, "output.", 7) == 0) {
// Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg // Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
const char *output_name = key + 7; const char *output_name = key + 7;
@ -160,18 +285,74 @@ static int parse_config_line(chroma_config_t *config, char *line,
return CHROMA_OK; return CHROMA_OK;
} }
// Validate image path // Check for extended output configuration with properties
if (chroma_image_validate(value) != CHROMA_OK) { // Format: output.DP-1.scale = fill
chroma_log("WARN", "Invalid image path for output %s: %s", output_name, // Format: output.DP-1.filter = linear
value); 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; 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, 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; return CHROMA_ERROR_CONFIG;
} }
if (expanded_path) {
free(expanded_path);
}
} else { } else {
chroma_log("WARN", "Unknown configuration key line %d: %s", line_number, chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
key); key);
@ -303,6 +484,37 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
return NULL; 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 // Create a sample configuration file
int chroma_config_create_sample(const char *config_file) { int chroma_config_create_sample(const char *config_file) {
if (!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", "=== Configuration ===");
chroma_log("INFO", "Default image: %s", config->default_image); chroma_log("INFO", "Default image: %s", config->default_image);
chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false"); 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); chroma_log("INFO", "Output mappings: %d", config->mapping_count);
for (int i = 0; i < config->mapping_count; i++) { for (int i = 0; i < config->mapping_count; i++) {
chroma_log("INFO", " %s -> %s", config->mappings[i].output_name, chroma_log("INFO", " %s -> %s (scale: %s, filter: %s)",
config->mappings[i].image_path); 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( chroma_log(
"TRACE", " Mapping %d: output='%s', image='%s', path_exists=%s", i, "TRACE",
config->mappings[i].output_name, config->mappings[i].image_path, " 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_path_exists(config->mappings[i].image_path) ? "yes" : "no");
} }
chroma_log("INFO", "===================="); chroma_log("INFO", "====================");

View file

@ -103,6 +103,32 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
// Assign image to output // Assign image to output
output->image = image; 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 // Create surface if it doesn't exist
if (!output->surface) { if (!output->surface) {
int ret = chroma_surface_create(state, output); int ret = chroma_surface_create(state, output);

View file

@ -4,6 +4,7 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
#include "../include/chroma.h" #include "../include/chroma.h"

View file

@ -8,6 +8,146 @@
#include "../include/chroma.h" #include "../include/chroma.h"
#include "../include/stb_image.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 // Vertex shader for simple texture rendering
static const char *vertex_shader_source = static const char *vertex_shader_source =
"#version 120\n" "#version 120\n"
@ -125,7 +265,7 @@ static int init_gl_resources(chroma_output_t *output) {
glGenBuffers(1, &output->ebo); glGenBuffers(1, &output->ebo);
glBindBuffer(GL_ARRAY_BUFFER, output->vbo); 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); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, 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 // Create or update texture from image data
static int update_texture_from_image(chroma_output_t *output, 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) { if (!output || !image || !image->loaded) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
} }
@ -180,8 +321,12 @@ static int update_texture_from_image(chroma_output_t *output,
// Set texture parameters // Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 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_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) // Upload texture data (always RGBA now)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, 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 (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); chroma_log("ERROR", "Failed to update texture for output %u", output->id);
return CHROMA_ERROR_EGL; 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); glBindTexture(GL_TEXTURE_2D, output->texture_id);
glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0); 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); glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(dynamic_vertices),
dynamic_vertices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
// Set vertex attributes // Set vertex attributes

View file

@ -94,18 +94,138 @@ void chroma_cleanup_signals(void) {
g_config_file = NULL; 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) { char *chroma_expand_path(const char *path) {
if (!path) { if (!path) {
return NULL; return NULL;
} }
if (path[0] != '~') { // First expand environment variables
return strdup(path); 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; const char *home;
if (path[1] == '/' || path[1] == '\0') { if (env_expanded[1] == '/' || env_expanded[1] == '\0') {
// ~/... or just ~ // ~/... or just ~
home = getenv("HOME"); home = getenv("HOME");
if (!home) { if (!home) {
@ -116,34 +236,39 @@ char *chroma_expand_path(const char *path) {
} }
if (!home) { if (!home) {
chroma_log("ERROR", "Could not determine home directory"); chroma_log("ERROR", "Could not determine home directory");
free(env_expanded);
return strdup(path); // Return original path as fallback return strdup(path); // Return original path as fallback
} }
size_t home_len = strlen(home); 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 char *expanded = malloc(home_len + path_len); // -1 for ~ +1 for \0
if (!expanded) { if (!expanded) {
chroma_log("ERROR", "Failed to allocate memory for path expansion"); chroma_log("ERROR", "Failed to allocate memory for path expansion");
free(env_expanded);
return strdup(path); return strdup(path);
} }
strcpy(expanded, home); strcpy(expanded, home);
if (path[1] == '/') { if (env_expanded[1] == '/') {
strcat(expanded, path + 1); strcat(expanded, env_expanded + 1);
} }
free(env_expanded);
return expanded; return expanded;
} else { } else {
// ~user/... // ~user/...
const char *slash = strchr(path, '/'); const char *slash = strchr(env_expanded, '/');
size_t user_len = slash ? (size_t)(slash - path - 1) : strlen(path) - 1; size_t user_len =
slash ? (size_t)(slash - env_expanded - 1) : strlen(env_expanded) - 1;
char *username = malloc(user_len + 1); char *username = malloc(user_len + 1);
if (!username) { if (!username) {
free(env_expanded);
return strdup(path); return strdup(path);
} }
strncpy(username, path + 1, user_len); strncpy(username, env_expanded + 1, user_len);
username[user_len] = '\0'; username[user_len] = '\0';
struct passwd *pw = getpwnam(username); struct passwd *pw = getpwnam(username);
@ -151,6 +276,7 @@ char *chroma_expand_path(const char *path) {
if (!pw) { if (!pw) {
chroma_log("ERROR", "User not found: %s", username); chroma_log("ERROR", "User not found: %s", username);
free(username); free(username);
free(env_expanded);
return strdup(path); return strdup(path);
} }
@ -160,6 +286,7 @@ char *chroma_expand_path(const char *path) {
size_t remaining_len = slash ? strlen(slash) : 0; size_t remaining_len = slash ? strlen(slash) : 0;
char *expanded = malloc(home_len + remaining_len + 1); char *expanded = malloc(home_len + remaining_len + 1);
if (!expanded) { if (!expanded) {
free(env_expanded);
return strdup(path); return strdup(path);
} }
@ -168,6 +295,7 @@ char *chroma_expand_path(const char *path) {
strcat(expanded, slash); strcat(expanded, slash);
} }
free(env_expanded);
return expanded; return expanded;
} }
} }