Compare commits
7 commits
ca468ce677
...
74f46f45bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
74f46f45bf |
|||
|
5fd2e5660f |
|||
|
1a366d2445 |
|||
|
e7f107a8fe |
|||
|
9b42e70054 |
|||
|
3d42f75052 |
|||
|
74fed80a26 |
9 changed files with 680 additions and 52 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -9,3 +9,5 @@ src/wlr-layer-shell-unstable-v1.c
|
|||
|
||||
# Ignore test stuff that I create to... test stuff.
|
||||
test_*
|
||||
*.jpg
|
||||
*.conf
|
||||
|
|
|
|||
55
Makefile
55
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,20 +150,53 @@ 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:"
|
||||
|
|
@ -161,12 +204,14 @@ help:
|
|||
@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-%:
|
||||
|
|
|
|||
35
include/chroma.h
vendored
35
include/chroma.h
vendored
|
|
@ -12,7 +12,8 @@
|
|||
#include <wayland-client.h>
|
||||
#include <wayland-egl.h>
|
||||
|
||||
#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);
|
||||
|
|
|
|||
8
include/chroma_version.h
Normal file
8
include/chroma_version.h
Normal 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
|
||||
270
src/config.c
270
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);
|
||||
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'",
|
||||
strlen(value), value);
|
||||
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 (add_output_mapping(config, output_name, value) != 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,
|
||||
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(
|
||||
"TRACE", " Mapping %d: output='%s', image='%s', path_exists=%s", 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", "====================");
|
||||
|
|
|
|||
26
src/core.c
26
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);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "../include/chroma.h"
|
||||
|
|
|
|||
175
src/render.c
175
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
|
||||
|
|
|
|||
148
src/utils.c
148
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue