Compare commits
18 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
29c298f71b |
|||
|
9c813b2fa0 |
|||
|
de6e8220e7 |
|||
|
f4275cb0f8 |
|||
|
f032d3723d |
|||
|
edae535674 |
|||
|
26eda26620 |
|||
|
e1f534d0e6 |
|||
|
412ae1b933 |
|||
|
0c1e373dd9 |
|||
|
c8ba7c4868 |
|||
|
40227627c1 |
|||
|
c84819b3e8 |
|||
|
8df203732e |
|||
|
c0b893887d |
|||
|
5258a0b492 |
|||
|
7306bbc625 |
|||
| 50c41fa883 |
17 changed files with 3687 additions and 647 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# Vendored headers are vendored code, to the surprise of absolutely noone.
|
||||
# See:
|
||||
# <https://github.com/github-linguist/linguist/blob/main/docs/overrides.md#vendored-code>
|
||||
/include/chroma.h linguist-vendored
|
||||
/include/**/* linguist-vendored
|
||||
|
||||
# Don't think linguist can detect XML, but let's tell it that the protocols are
|
||||
# vendored *anyway*.
|
||||
|
|
|
|||
27
Makefile
27
Makefile
|
|
@ -1,5 +1,5 @@
|
|||
PROJECT_NAME = chroma
|
||||
VERSION = 1.0.1
|
||||
VERSION = 2.0.0
|
||||
|
||||
# Directories
|
||||
SRCDIR = src
|
||||
|
|
@ -14,6 +14,9 @@ PREFIX ?= /usr/local
|
|||
BINDIR_INSTALL = $(PREFIX)/bin
|
||||
SYSTEMD_INSTALL = $(HOME)/.config/systemd/user
|
||||
|
||||
# Config file
|
||||
CONFIG_FILE_NAME = chroma.toml
|
||||
|
||||
# Compiler and flags
|
||||
CC = gcc
|
||||
CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g
|
||||
|
|
@ -46,8 +49,11 @@ LDFLAGS += -lm -ldl
|
|||
|
||||
# Source files (excluding generated protocol files)
|
||||
SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))
|
||||
VENDOR_SOURCES = $(INCDIR)/vendor/tomlc17.c
|
||||
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)
|
||||
DEPENDS = $(OBJECTS:.o=.d)
|
||||
VENDOR_OBJECTS = $(VENDOR_SOURCES:$(INCDIR)/vendor/%.c=$(OBJDIR)/%.o)
|
||||
ALL_OBJECTS = $(OBJECTS) $(VENDOR_OBJECTS)
|
||||
DEPENDS = $(ALL_OBJECTS:.o=.d)
|
||||
|
||||
# Override object files for image.c and render.c to suppress third-party warnings
|
||||
OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS))
|
||||
|
|
@ -78,9 +84,9 @@ $(INCDIR):
|
|||
@mkdir -p $(INCDIR)
|
||||
|
||||
# Build main executable
|
||||
$(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
|
||||
$(TARGET): version-header $(PROTOCOL_HEADERS) $(ALL_OBJECTS) | $(BINDIR)
|
||||
@echo " LINK $@"
|
||||
@$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
|
||||
@$(CC) $(ALL_OBJECTS) -o $@ $(LDFLAGS)
|
||||
|
||||
# Compile source files
|
||||
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)
|
||||
|
|
@ -95,6 +101,11 @@ $(OBJDIR)/render.o: $(SRCDIR)/render.c $(PROTOCOL_HEADERS) | $(OBJDIR)
|
|||
@echo " CC $<"
|
||||
@$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@
|
||||
|
||||
# Compile vendor files
|
||||
$(OBJDIR)/%.o: $(INCDIR)/vendor/%.c | $(OBJDIR)
|
||||
@echo " CC $<"
|
||||
@$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@
|
||||
|
||||
# Debug build
|
||||
debug: CFLAGS = $(DEBUG_CFLAGS)
|
||||
debug: $(TARGET)
|
||||
|
|
@ -142,6 +153,11 @@ version-header:
|
|||
# Create systemd service file
|
||||
systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service
|
||||
|
||||
# Create sample configuration file
|
||||
sample-config:
|
||||
@echo "Creating sample configuration..."
|
||||
@cp chroma.toml.sample $(CONFIG_FILE_NAME)
|
||||
@echo "Sample configuration created at $(CONFIG_FILE_NAME)"
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
|
|
@ -230,6 +246,7 @@ help:
|
|||
@echo " check-deps - Check if all dependencies are available"
|
||||
@echo " install - Install executable and systemd service"
|
||||
@echo " uninstall - Remove installed files"
|
||||
@echo " sample-config - Create sample configuration file"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " distclean - Remove all generated files"
|
||||
@echo " format - Format source code (requires clang-format)"
|
||||
|
|
@ -254,7 +271,7 @@ help:
|
|||
-include $(DEPENDS)
|
||||
|
||||
# Phony targets
|
||||
.PHONY: all debug static check-deps install uninstall systemd-service version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version
|
||||
.PHONY: all debug static check-deps install uninstall systemd-service sample-config version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version
|
||||
|
||||
# Print variables
|
||||
print-%:
|
||||
|
|
|
|||
48
README.md
48
README.md
|
|
@ -28,7 +28,7 @@ comprehensive monitor management. Here's what makes Chroma stand out:
|
|||
- **Multiple image formats**: Supports JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC,
|
||||
PPM, PGM
|
||||
- **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering
|
||||
- **Configuration file support**: Easy setup with INI-style config files
|
||||
- **Simple Configuration file**: Easy setup with TOML config files
|
||||
- **Signal handling**: Graceful shutdown and configuration reload (SIGHUP)
|
||||
- **Intelligent downsampling**: Automatically reduces large image resolution to
|
||||
save memory (up to 94% memory savings for 8K images)
|
||||
|
|
@ -96,21 +96,40 @@ make sample-config
|
|||
|
||||
Chroma looks for configuration files in this order:
|
||||
|
||||
1. `~/.config/chroma/chroma.conf`
|
||||
2. `$XDG_CONFIG_HOME/chroma/chroma.conf`
|
||||
3. `./chroma.conf` (current directory)
|
||||
1. `$XDG_CONFIG_HOME/chroma/chroma.toml`
|
||||
2. `~/.config/chroma/chroma.toml`
|
||||
3. `./chroma.toml` (current directory)
|
||||
|
||||
#### Sample Configuration
|
||||
|
||||
```ini
|
||||
```toml
|
||||
# Default wallpaper for outputs without specific mapping
|
||||
default_image = ~/.config/chroma/default.jpg
|
||||
default_image = "~/.config/chroma/default.jpg"
|
||||
|
||||
# Output-specific wallpapers
|
||||
# Format: output.OUTPUT_NAME = /path/to/image.jpg
|
||||
output.DP-1 = ~/Pictures/monitor1.jpg
|
||||
output.DP-2 = ~/Pictures/monitor2.png
|
||||
output.HDMI-A-1 = ~/Pictures/hdmi_wallpaper.jpg
|
||||
# Format: [[output]] with name and image keys
|
||||
[[output]]
|
||||
name = "DP-1"
|
||||
image = "~/Pictures/monitor1.jpg"
|
||||
|
||||
[[output]]
|
||||
name = "DP-2"
|
||||
image = "~/Pictures/monitor2.png"
|
||||
|
||||
[[output]]
|
||||
name = "HDMI-A-1"
|
||||
image = "~/Pictures/hdmi_wallpaper.jpg"
|
||||
|
||||
# You can also match outputs by description using the desc: prefix
|
||||
# This is useful when output names change between reboots
|
||||
# Format: output."desc:DESCRIPTION_PREFIX" = "/path/to/image.jpg"
|
||||
[[output]]
|
||||
name = "desc:Samsung"
|
||||
image = "~/Pictures/samsung_wallpaper.jpg"
|
||||
|
||||
[[output]]
|
||||
name = "desc:LG Ultra"
|
||||
image = "~/Pictures/lg_wallpaper.jpg"
|
||||
```
|
||||
|
||||
### Finding Output Names
|
||||
|
|
@ -134,7 +153,7 @@ Options:
|
|||
--version Show version information
|
||||
|
||||
Examples:
|
||||
chroma -c ~/.config/chroma/chroma.conf
|
||||
chroma -c ~/.config/chroma/chroma.toml
|
||||
chroma --daemon
|
||||
```
|
||||
|
||||
|
|
@ -183,10 +202,13 @@ Chroma works with any Wayland compositor that supports:
|
|||
|
||||
- `wl_compositor` interface
|
||||
- `wl_output` interface
|
||||
- `zwlr_layer_shell_v1` (wlr-layer-shell-unstable-v1) interface
|
||||
- EGL window surface creation
|
||||
|
||||
Tested only on Hyprland, but should work fine with any compositor that meets the
|
||||
above criteria. Which is basically all of them I think?
|
||||
The wlr-layer-shell protocol is required for creating wallpaper surfaces on the
|
||||
background layer. Tested only on Hyprland, but should work fine with any
|
||||
wlroots-based compositor or any compositor that implements the wlr-layer-shell
|
||||
protocol.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
|||
|
|
@ -1,134 +0,0 @@
|
|||
# This is a sample configuration file for the Chroma wallpaper daemon.
|
||||
# Lines starting with # are comments and are ignored.
|
||||
#
|
||||
# Configuration file locations (checked in order):
|
||||
# 1. ~/.config/chroma/chroma.conf
|
||||
# 2. $XDG_CONFIG_HOME/chroma/chroma.conf
|
||||
# 3. ./chroma.conf (current directory)
|
||||
#
|
||||
# To get started:
|
||||
# 1. Copy this file to ~/.config/chroma/chroma.conf
|
||||
# 2. Modify the paths to point to your wallpaper images
|
||||
# 3. Use 'wlr-randr' or similar tools to find your output names
|
||||
# 4. Restart chroma or send SIGHUP to reload configuration
|
||||
|
||||
# This image will be used for any output that doesn't have a specific mapping.
|
||||
# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM
|
||||
# Paths can be absolute or relative, ~ expansion is supported.
|
||||
#
|
||||
# Alternative examples:
|
||||
# default_image = ~/Pictures/wallpapers/default.png
|
||||
# default_image = /usr/share/wallpapers/default.jpg
|
||||
# default_image = ./wallpapers/fallback.jpg
|
||||
default_image = ~/.config/chroma/default.jpg
|
||||
|
||||
|
||||
# Whether to run as a background daemon
|
||||
# Usually set via command line option --daemon, but can be set here too
|
||||
daemon_mode = false
|
||||
|
||||
# Global scaling mode for wallpapers (used as default for all outputs)
|
||||
# Options: fill, fit, stretch, center
|
||||
# fill - Fill entire output, crop if necessary (default)
|
||||
# fit - Fit image within output, add borders if needed
|
||||
# stretch - Stretch to fill output, may distort aspect ratio
|
||||
# center - Center image at original size
|
||||
scale_mode = fill
|
||||
|
||||
# Global image filtering quality (used as default for all outputs)
|
||||
# Options: nearest, linear, bilinear, trilinear
|
||||
# nearest - Nearest neighbor filtering (pixelated, fast)
|
||||
# linear - Linear filtering (smooth, default)
|
||||
# bilinear - Bilinear filtering (smoother)
|
||||
# trilinear - Trilinear filtering (smoothest)
|
||||
filter_quality = linear
|
||||
|
||||
# Default anchor position (0-100 for both x and y)
|
||||
# anchor_x: 0=left, 50=center, 100=right
|
||||
# anchor_y: 0=top, 50=center, 100=bottom
|
||||
# Can use named anchors: center, top, bottom, left, right, top-left, etc.
|
||||
anchor = center
|
||||
anchor_x = 50
|
||||
anchor_y = 50
|
||||
|
||||
# Image downsampling settings for performance optimization
|
||||
# ========================================================
|
||||
# Enable automatic downsampling of large images to save memory and improve performance
|
||||
# Set to false to keep original resolution for all images (uses more memory!)
|
||||
enable_downsampling = true
|
||||
|
||||
# Maximum expected output resolution
|
||||
# Images larger than these dimensions may be automatically downsampled
|
||||
# Adjust based on your actual monitor setup
|
||||
max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p)
|
||||
max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p)
|
||||
|
||||
# Minimum scale factor
|
||||
# Prevents images from being scaled below this percentage of original size
|
||||
# Useful to preserve detail on very high-resolution images
|
||||
# Range: 0.1 to 1.0 (10% to 100%)
|
||||
min_scale_factor = 0.25 # Don't scale below 25% of original size
|
||||
|
||||
# Output-specific wallpaper mappings
|
||||
# ==================================
|
||||
# Basic format: output.OUTPUT_NAME = /path/to/image.ext
|
||||
#
|
||||
# You can match outputs by name OR by description:
|
||||
# output.DP-1 = /path/to/image.jpg # Match by port name
|
||||
# output.desc:Samsung = /path/to/image.jpg # Match by description prefix
|
||||
#
|
||||
# The description is the human-readable name provided by the compositor
|
||||
# via the Wayland wl_output description event. For example, if your
|
||||
# monitor reports "Samsung T27A450" as its description, you can use
|
||||
# "output.desc:Samsung" to match it. The match is a prefix match, so
|
||||
# "output.desc:Sam" would also work.
|
||||
#
|
||||
# To find your output names and descriptions, run one of these commands:
|
||||
# wlr-randr (for wlroots-based compositors)
|
||||
# wayland-info | grep wl_output
|
||||
# kanshi list-outputs
|
||||
#
|
||||
# Extended format with per-output settings:
|
||||
# output.OUTPUT_NAME = /path/to/image.ext
|
||||
# output.OUTPUT_NAME.scale = fill|fit|stretch|center
|
||||
# output.OUTPUT_NAME.filter = nearest|linear|bilinear|trilinear
|
||||
# output.OUTPUT_NAME.anchor = center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right
|
||||
#
|
||||
# Anchor specifies which part of the image is anchored to the output:
|
||||
# center - image centered (default)
|
||||
# top - image anchored to top edge
|
||||
# bottom - image anchored to bottom edge
|
||||
# left - image anchored to left edge
|
||||
# right - image anchored to right edge
|
||||
# top-left - image anchored to top-left corner
|
||||
# top-right - image anchored to top-right corner
|
||||
# bottom-left - image anchored to bottom-left corner
|
||||
# bottom-right - image anchored to bottom-right corner
|
||||
#
|
||||
# Examples:
|
||||
# output.HDMI-A-1 = ~/Pictures/wallpaper.jpg
|
||||
# output.DP-1 = ~/Pictures/monitor1.png
|
||||
# output.DP-1.scale = fit
|
||||
# output.DP-1.filter = bilinear
|
||||
# output.DP-1.anchor = top-left
|
||||
# output.DP-2 = ~/Pictures/monitor2.jpg
|
||||
# output.DP-2.scale = stretch
|
||||
#
|
||||
# # Match by monitor description (prefix match):
|
||||
# output.desc:Samsung = ~/Pictures/samsung-wallpaper.jpg
|
||||
# output.desc:Samsung.scale = fill
|
||||
# output.desc:LG Ultra = ~/Pictures/lg-wallpaper.jpg
|
||||
# output.desc:BenQ = ~/Pictures/benq-wallpaper.jpg
|
||||
#
|
||||
# # Laptop internal display:
|
||||
# output.eDP-1 = ~/Pictures/laptop-wallpaper.jpg
|
||||
# output.eDP-1.scale = fill
|
||||
# output.eDP-1.anchor = bottom-right
|
||||
# output.eDP-1.filter = trilinear
|
||||
#
|
||||
# Custom anchor coordinates (anchor_x, anchor_y override anchor):
|
||||
# output.ULTRAWIDE = ~/Pictures/ultrawide.jpg
|
||||
# output.ULTRAWIDE.scale = fill
|
||||
# output.ULTRAWIDE.anchor_x = 25 # 25% from left edge
|
||||
# output.ULTRAWIDE.anchor_y = 10 # 10% from top edge
|
||||
|
||||
127
chroma.toml.sample
Normal file
127
chroma.toml.sample
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
# This is a sample TOML configuration file for the Chroma wallpaper daemon.
|
||||
# For more information about TOML format, see https://toml.io
|
||||
#
|
||||
# Configuration file locations (checked in order):
|
||||
# 1. $XDG_CONFIG_HOME/chroma/chroma.toml
|
||||
# 2. ~/.config/chroma/chroma.toml
|
||||
# 3. ./chroma.toml (current directory)
|
||||
#
|
||||
# To get started:
|
||||
# 1. Copy this file to ~/.config/chroma/chroma.toml
|
||||
# 2. Modify the paths to point to your wallpaper images
|
||||
# 3. Use 'wlr-randr' or similar tools to find your output names
|
||||
# 4. Restart chroma or send SIGHUP to reload configuration
|
||||
|
||||
# Default wallpaper for outputs without specific mapping
|
||||
# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM
|
||||
# Paths can be absolute or relative, ~ expansion is supported.
|
||||
default_image = "~/.config/chroma/default.jpg"
|
||||
|
||||
# Whether to run as a background daemon
|
||||
# Usually set via command line option --daemon, but can be set here too
|
||||
daemon_mode = false
|
||||
|
||||
# Global scaling mode for wallpapers (used as default for all outputs)
|
||||
# Options: fill, fit, stretch, center
|
||||
# fill - Fill entire output, crop if necessary (default)
|
||||
# fit - Fit image within output, add borders if needed
|
||||
# stretch - Stretch to fill output, may distort aspect ratio
|
||||
# center - Center image at original size
|
||||
scale_mode = "fill"
|
||||
|
||||
# Global image filtering quality (used as default for all outputs)
|
||||
# Options: nearest, linear, bilinear, trilinear
|
||||
# nearest - Nearest neighbor filtering (pixelated, fast)
|
||||
# linear - Linear filtering (smooth, default)
|
||||
# bilinear - Bilinear filtering (smoother)
|
||||
# trilinear - Trilinear filtering (smoothest)
|
||||
filter_quality = "linear"
|
||||
|
||||
# Default anchor position (0-100 for both x and y)
|
||||
# anchor_x: 0=left, 50=center, 100=right
|
||||
# anchor_y: 0=top, 50=center, 100=bottom
|
||||
# Can use named anchors: center, top, bottom, left, right, top-left, etc.
|
||||
anchor = "center"
|
||||
anchor_x = 50
|
||||
anchor_y = 50
|
||||
|
||||
# Image downsampling settings for performance optimization
|
||||
[downsampling]
|
||||
# Enable automatic downsampling of large images to save memory and improve performance
|
||||
# Set to false to keep original resolution for all images (uses more memory!)
|
||||
enable = true
|
||||
|
||||
# Maximum expected output resolution
|
||||
# Images larger than these dimensions may be automatically downsampled
|
||||
# Adjust based on your actual monitor setup
|
||||
max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p)
|
||||
max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p)
|
||||
|
||||
# Minimum scale factor
|
||||
# Prevents images from being scaled below this percentage of original size
|
||||
# Useful to preserve detail on very high-resolution images
|
||||
# Range: 0.1 to 1.0 (10% to 100%)
|
||||
min_scale_factor = 0.25 # Don't scale below 25% of original size
|
||||
|
||||
# Output-specific wallpaper mappings
|
||||
# ==================================
|
||||
# Each [[output]] block defines a mapping for a specific output.
|
||||
#
|
||||
# You can match outputs by name OR by description:
|
||||
# name = "DP-1" # Match by port name
|
||||
# name = "desc:Samsung" # Match by description prefix
|
||||
#
|
||||
# The description is the human-readable name provided by the compositor
|
||||
# via the Wayland wl_output description event. For example, if your
|
||||
# monitor reports "Samsung T27A450" as its description, you can use
|
||||
# "desc:Samsung" to match it. The match is a prefix match, so
|
||||
# "desc:Sam" would also work.
|
||||
#
|
||||
# To find your output names and descriptions, run one of these commands:
|
||||
# wlr-randr (for wlroots-based compositors)
|
||||
# wayland-info | grep wl_output
|
||||
# kanshi list-outputs
|
||||
|
||||
[[output]]
|
||||
name = "DP-1"
|
||||
image = "~/Pictures/monitor1.jpg"
|
||||
scale = "fill"
|
||||
filter = "linear"
|
||||
anchor = "center"
|
||||
|
||||
[[output]]
|
||||
name = "DP-2"
|
||||
image = "~/Pictures/monitor2.png"
|
||||
scale = "fit"
|
||||
|
||||
[[output]]
|
||||
name = "HDMI-A-1"
|
||||
image = "~/Pictures/hdmi_wallpaper.jpg"
|
||||
scale = "stretch"
|
||||
|
||||
# Match by monitor description (prefix match):
|
||||
[[output]]
|
||||
name = "desc:Samsung"
|
||||
image = "~/Pictures/samsung-wallpaper.jpg"
|
||||
scale = "fill"
|
||||
|
||||
[[output]]
|
||||
name = "desc:LG Ultra"
|
||||
image = "~/Pictures/lg-wallpaper.jpg"
|
||||
scale = "fit"
|
||||
|
||||
# Laptop internal display:
|
||||
[[output]]
|
||||
name = "eDP-1"
|
||||
image = "~/Pictures/laptop-wallpaper.jpg"
|
||||
scale = "fill"
|
||||
anchor = "bottom-right"
|
||||
filter = "trilinear"
|
||||
|
||||
# Custom anchor coordinates (anchor_x, anchor_y override anchor):
|
||||
[[output]]
|
||||
name = "ULTRAWIDE"
|
||||
image = "~/Pictures/ultrawide.jpg"
|
||||
scale = "fill"
|
||||
anchor_x = 25 # 25% from left edge
|
||||
anchor_y = 10 # 10% from top edge
|
||||
6
flake.lock
generated
6
flake.lock
generated
|
|
@ -2,11 +2,11 @@
|
|||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1776074868,
|
||||
"narHash": "sha256-XwIXdLWyLhr+7rCjKBc0i54ExC6/s0XEC4jT8ozI2S4=",
|
||||
"lastModified": 1777312179,
|
||||
"narHash": "sha256-AyQh4VtqwzVeLO1uHZ6/pzS6O96nXCXiUQZgCG+3X6k=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "fbb4cb95f81097fa79dae76545e2e7f6dc311f93",
|
||||
"rev": "20123554ae9affff1a5bc969ad3ad02e839f49cf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
|
|||
17
include/chroma.h
vendored
17
include/chroma.h
vendored
|
|
@ -4,7 +4,7 @@
|
|||
#include "wlr-layer-shell-unstable-v1.h"
|
||||
#include "xdg-shell.h"
|
||||
#include <EGL/egl.h>
|
||||
#include <GL/gl.h>
|
||||
#include <GLES2/gl2.h>
|
||||
#include <signal.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
#define MAX_OUTPUTS 16
|
||||
#define MAX_PATH_LEN 4096
|
||||
#define CONFIG_FILE_NAME "chroma.conf"
|
||||
#define CONFIG_FILE_NAME "chroma.toml"
|
||||
|
||||
// Log levels
|
||||
#define CHROMA_LOG_ERROR 0
|
||||
|
|
@ -73,6 +73,7 @@ typedef struct {
|
|||
int channels;
|
||||
char path[MAX_PATH_LEN];
|
||||
bool loaded;
|
||||
int ref_count; // Number of outputs using this image
|
||||
} chroma_image_t;
|
||||
|
||||
// Wayland output information
|
||||
|
|
@ -116,6 +117,7 @@ typedef struct {
|
|||
bool gl_resources_initialized;
|
||||
bool texture_uploaded;
|
||||
bool vbo_dirty; // track VBO needs update
|
||||
bool configured; // track if initial configure received
|
||||
} chroma_output_t;
|
||||
|
||||
// Config mapping structure
|
||||
|
|
@ -162,6 +164,9 @@ typedef struct chroma_state {
|
|||
EGLDisplay egl_display;
|
||||
EGLContext egl_context;
|
||||
EGLConfig egl_config;
|
||||
|
||||
// Shared OpenGL resources
|
||||
GLuint shader_program;
|
||||
|
||||
// Outputs
|
||||
chroma_output_t outputs[MAX_OUTPUTS];
|
||||
|
|
@ -235,12 +240,15 @@ void chroma_layer_surface_closed(void *data,
|
|||
// Image loading
|
||||
void chroma_image_init_stb(void);
|
||||
int chroma_image_load(chroma_image_t *image, const char *path,
|
||||
const chroma_config_t *config);
|
||||
const chroma_config_t *config, int output_width,
|
||||
int output_height);
|
||||
void chroma_image_free(chroma_image_t *image);
|
||||
void chroma_image_release(chroma_image_t *image);
|
||||
chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
|
||||
const char *path);
|
||||
chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
|
||||
const char *path);
|
||||
const char *path, int output_width,
|
||||
int output_height);
|
||||
int chroma_image_validate(const char *path);
|
||||
int chroma_image_get_info(const char *path, int *width, int *height,
|
||||
int *channels);
|
||||
|
|
@ -248,6 +256,7 @@ void chroma_images_cleanup(chroma_state_t *state);
|
|||
|
||||
// Configuration
|
||||
int chroma_config_load(chroma_config_t *config, const char *config_file);
|
||||
int chroma_config_load_toml(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,
|
||||
|
|
|
|||
2
include/chroma_version.h
vendored
2
include/chroma_version.h
vendored
|
|
@ -2,7 +2,7 @@
|
|||
#define CHROMA_VERSION_H
|
||||
|
||||
#ifndef CHROMA_VERSION
|
||||
#define CHROMA_VERSION "1.0.1"
|
||||
#define CHROMA_VERSION "2.0.0"
|
||||
#endif
|
||||
|
||||
#endif // CHROMA_VERSION_H
|
||||
|
|
|
|||
2915
include/vendor/tomlc17.c
vendored
Normal file
2915
include/vendor/tomlc17.c
vendored
Normal file
File diff suppressed because it is too large
Load diff
190
include/vendor/tomlc17.h
vendored
Normal file
190
include/vendor/tomlc17.h
vendored
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/* Copyright (c) 2024-2026, CK Tan.
|
||||
* https://github.com/cktan/tomlc17/blob/main/LICENSE
|
||||
*/
|
||||
#ifndef TOMLC17_H
|
||||
#define TOMLC17_H
|
||||
|
||||
/*
|
||||
* USAGE:
|
||||
*
|
||||
* 1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex()
|
||||
* 2. Check result.ok
|
||||
* 3. Use toml_get() or toml_seek() to query and traverse the
|
||||
* result.toptab
|
||||
* 4. Call toml_free() to release resources.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define TOML_EXTERN extern "C"
|
||||
#else
|
||||
#define TOML_EXTERN extern
|
||||
#endif
|
||||
|
||||
enum toml_type_t {
|
||||
TOML_UNKNOWN = 0,
|
||||
TOML_STRING,
|
||||
TOML_INT64,
|
||||
TOML_FP64,
|
||||
TOML_BOOLEAN,
|
||||
TOML_DATE,
|
||||
TOML_TIME,
|
||||
TOML_DATETIME,
|
||||
TOML_DATETIMETZ,
|
||||
TOML_ARRAY,
|
||||
TOML_TABLE,
|
||||
};
|
||||
typedef enum toml_type_t toml_type_t;
|
||||
|
||||
/* This is a Node in a Tree that represents a toml document rooted
|
||||
* at toml_result_t::toptab.
|
||||
*/
|
||||
typedef struct toml_datum_t toml_datum_t;
|
||||
struct toml_datum_t {
|
||||
toml_type_t type;
|
||||
uint32_t flag; // internal
|
||||
union {
|
||||
const char *s; // same as str.ptr; use if there are no NUL in string.
|
||||
struct {
|
||||
const char *ptr; // NUL terminated string
|
||||
int len; // length excluding the terminating NUL.
|
||||
} str;
|
||||
int64_t int64; // integer
|
||||
double fp64; // float
|
||||
bool boolean;
|
||||
struct { // date, time
|
||||
int16_t year, month, day;
|
||||
int16_t hour, minute, second;
|
||||
int32_t usec;
|
||||
int16_t tz; // in minutes
|
||||
} ts;
|
||||
struct { // array
|
||||
int32_t size; // count elem
|
||||
toml_datum_t *elem; // elem[]
|
||||
} arr;
|
||||
struct { // table
|
||||
int32_t size; // count key
|
||||
const char **key; // key[]
|
||||
int *len; // len[]
|
||||
toml_datum_t *value; // value[]
|
||||
} tab;
|
||||
} u;
|
||||
};
|
||||
|
||||
/* Result returned by toml_parse() */
|
||||
typedef struct toml_result_t toml_result_t;
|
||||
struct toml_result_t {
|
||||
bool ok; // success flag
|
||||
toml_datum_t toptab; // valid if ok
|
||||
char errmsg[200]; // valid if not ok
|
||||
void *__internal; // do not use
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse a toml document. Returns a toml_result which must be freed
|
||||
* using toml_free() eventually.
|
||||
*
|
||||
* IMPORTANT: src[] must be a NUL terminated string! The len parameter
|
||||
* does not include the NUL terminator.
|
||||
*/
|
||||
TOML_EXTERN toml_result_t toml_parse(const char *src, int len);
|
||||
|
||||
/**
|
||||
* Parse a toml file. Returns a toml_result which must be freed
|
||||
* using toml_free() eventually.
|
||||
*
|
||||
* IMPORTANT: you are still responsible to fclose(fp).
|
||||
*/
|
||||
TOML_EXTERN toml_result_t toml_parse_file(FILE *fp);
|
||||
|
||||
/**
|
||||
* Parse a toml file. Returns a toml_result which must be freed
|
||||
* using toml_free() eventually.
|
||||
*/
|
||||
TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname);
|
||||
|
||||
/**
|
||||
* Release the result.
|
||||
*/
|
||||
TOML_EXTERN void toml_free(toml_result_t result);
|
||||
|
||||
/**
|
||||
* Find a key in a toml_table. Return the value of the key if found,
|
||||
* or a TOML_UNKNOWN otherwise.
|
||||
*/
|
||||
TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key);
|
||||
|
||||
/**
|
||||
* Locate a value starting from a toml_table. Return the value of the key if
|
||||
* found, or a TOML_UNKNOWN otherwise.
|
||||
*
|
||||
* Note: the multipart-key is separated by DOT, and must not have any escape
|
||||
* chars. The maximum length of the multipart_key must not exceed 127 bytes.
|
||||
*/
|
||||
TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table,
|
||||
const char *multipart_key);
|
||||
|
||||
/**
|
||||
* OBSOLETE: use toml_get() instead.
|
||||
* Find a key in a toml_table. Return the value of the key if found,
|
||||
* or a TOML_UNKNOWN otherwise. (
|
||||
*/
|
||||
static inline toml_datum_t toml_table_find(toml_datum_t table,
|
||||
const char *key) {
|
||||
return toml_get(table, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override values in r1 using r2. Return a new result. All results
|
||||
* (i.e., r1, r2 and the returned result) must be freed using toml_free()
|
||||
* after use.
|
||||
*
|
||||
* LOGIC:
|
||||
* ret = copy of r1
|
||||
* for each item x in r2:
|
||||
* if x is not in ret:
|
||||
* override
|
||||
* elif x in ret is NOT of the same type:
|
||||
* override
|
||||
* elif x is an array of tables:
|
||||
* append r2.x to ret.x
|
||||
* elif x is a table:
|
||||
* merge r2.x to ret.x
|
||||
* else:
|
||||
* override
|
||||
*/
|
||||
TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1,
|
||||
const toml_result_t *r2);
|
||||
|
||||
/**
|
||||
* Check if two results are the same. Dictionary and array orders are
|
||||
* sensitive.
|
||||
*/
|
||||
TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2);
|
||||
|
||||
/* Options that override tomlc17 defaults globally */
|
||||
typedef struct toml_option_t toml_option_t;
|
||||
struct toml_option_t {
|
||||
bool check_utf8; // Check all chars are valid utf8; default: false.
|
||||
void *(*mem_realloc)(void *ptr, size_t size); // default: realloc()
|
||||
void (*mem_free)(void *ptr); // default: free()
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the default options. IF NECESSARY, use this to initialize
|
||||
* toml_option_t and override values before calling
|
||||
* toml_set_option().
|
||||
*/
|
||||
TOML_EXTERN toml_option_t toml_default_option(void);
|
||||
|
||||
/**
|
||||
* Set toml options globally. Do this ONLY IF you are not satisfied with the
|
||||
* defaults.
|
||||
*/
|
||||
TOML_EXTERN void toml_set_option(toml_option_t opt);
|
||||
|
||||
#endif // TOMLC17_H
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
in
|
||||
stdenv.mkDerivation {
|
||||
pname = "chroma";
|
||||
version = "1.0.1";
|
||||
version = "2.0.0";
|
||||
|
||||
src = fs.toSource {
|
||||
root = s;
|
||||
|
|
@ -64,7 +64,7 @@ in
|
|||
'';
|
||||
|
||||
postInstall = ''
|
||||
install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample
|
||||
install -Dm755 ${../chroma.toml.sample} $out/share/chroma.toml.sample
|
||||
'';
|
||||
|
||||
meta = {
|
||||
|
|
|
|||
612
src/config.c
612
src/config.c
|
|
@ -5,29 +5,9 @@
|
|||
#include <string.h>
|
||||
#include <strings.h>
|
||||
|
||||
#include "../include/chroma.h"
|
||||
#include "chroma.h"
|
||||
#include "vendor/tomlc17.h"
|
||||
|
||||
static char *trim_whitespace(char *str) {
|
||||
char *end;
|
||||
|
||||
// Trim leading whitespace
|
||||
while (isspace((unsigned char)*str))
|
||||
str++;
|
||||
|
||||
// All spaces?
|
||||
if (*str == 0)
|
||||
return str;
|
||||
|
||||
// Trim trailing whitespace
|
||||
end = str + strlen(str) - 1;
|
||||
while (end > str && isspace((unsigned char)*end))
|
||||
end--;
|
||||
|
||||
*(end + 1) = '\0';
|
||||
return str;
|
||||
}
|
||||
|
||||
// Remove quotes from a string
|
||||
// Match output name/description against a config pattern
|
||||
// Supports:
|
||||
// - Exact name match: "DP-1" matches wl_output.name == "DP-1"
|
||||
|
|
@ -60,31 +40,8 @@ static bool match_output(const char *pattern, const char *output_name,
|
|||
return false;
|
||||
}
|
||||
|
||||
static char *remove_quotes(char *str) {
|
||||
size_t len = strlen(str);
|
||||
if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
|
||||
(str[0] == '\'' && str[len - 1] == '\''))) {
|
||||
str[len - 1] = '\0';
|
||||
return str + 1;
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// Parse boolean value from string
|
||||
static bool parse_bool(const char *value) {
|
||||
if (!value)
|
||||
return false;
|
||||
|
||||
if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||
|
||||
strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse scaling mode from string
|
||||
static chroma_scale_mode_t parse_scale_mode(const char *value) {
|
||||
chroma_scale_mode_t parse_scale_mode(const char *value) {
|
||||
if (!value)
|
||||
return CHROMA_SCALE_FILL; // default
|
||||
|
||||
|
|
@ -103,7 +60,7 @@ static chroma_scale_mode_t parse_scale_mode(const char *value) {
|
|||
}
|
||||
|
||||
// Parse filter quality from string
|
||||
static chroma_filter_quality_t parse_filter_quality(const char *value) {
|
||||
chroma_filter_quality_t parse_filter_quality(const char *value) {
|
||||
if (!value)
|
||||
return CHROMA_FILTER_LINEAR; // default
|
||||
|
||||
|
|
@ -154,7 +111,7 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
|
|||
}
|
||||
|
||||
// Parse anchor position from string
|
||||
static chroma_anchor_t parse_anchor(const char *value) {
|
||||
chroma_anchor_t parse_anchor(const char *value) {
|
||||
if (!value)
|
||||
return CHROMA_ANCHOR_CENTER;
|
||||
|
||||
|
|
@ -213,12 +170,10 @@ static const char *anchor_to_string(chroma_anchor_t anchor) {
|
|||
}
|
||||
|
||||
// Output-to-image mapping
|
||||
static int add_output_mapping(chroma_config_t *config, const char *output_name,
|
||||
const char *image_path,
|
||||
chroma_scale_mode_t scale_mode,
|
||||
chroma_filter_quality_t filter_quality,
|
||||
chroma_anchor_t anchor, float anchor_x,
|
||||
float anchor_y) {
|
||||
int add_output_mapping(chroma_config_t *config, const char *output_name,
|
||||
const char *image_path, chroma_scale_mode_t scale_mode,
|
||||
chroma_filter_quality_t filter_quality,
|
||||
chroma_anchor_t anchor, float anchor_x, float anchor_y) {
|
||||
if (!config || !output_name || !image_path) {
|
||||
return CHROMA_ERROR_INIT;
|
||||
}
|
||||
|
|
@ -268,6 +223,194 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
|
|||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Parse TOML configuration file
|
||||
int chroma_config_load_toml(chroma_config_t *config, const char *config_file) {
|
||||
if (!config || !config_file) {
|
||||
return CHROMA_ERROR_INIT;
|
||||
}
|
||||
|
||||
toml_result_t result = toml_parse_file_ex(config_file);
|
||||
if (!result.ok) {
|
||||
chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg);
|
||||
toml_free(result);
|
||||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
|
||||
chroma_log("INFO", "Loading TOML configuration from: %s", config_file);
|
||||
|
||||
// Parse default_image
|
||||
toml_datum_t default_image = toml_seek(result.toptab, "default_image");
|
||||
if (default_image.type == TOML_STRING) {
|
||||
char *expanded_path = chroma_expand_path(default_image.u.s);
|
||||
const char *path_to_use = expanded_path ? expanded_path : default_image.u.s;
|
||||
if (strlen(path_to_use) < sizeof(config->default_image)) {
|
||||
strcpy(config->default_image, path_to_use);
|
||||
} else {
|
||||
chroma_log("WARN",
|
||||
"default_image path too long (%zu >= %zu), truncating: %s",
|
||||
strlen(path_to_use), sizeof(config->default_image), path_to_use);
|
||||
snprintf(config->default_image, sizeof(config->default_image), "%s",
|
||||
path_to_use);
|
||||
}
|
||||
if (expanded_path) {
|
||||
free(expanded_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse daemon_mode
|
||||
toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode");
|
||||
if (daemon_mode.type == TOML_BOOLEAN) {
|
||||
config->daemon_mode = daemon_mode.u.boolean;
|
||||
}
|
||||
|
||||
// Parse scale_mode
|
||||
toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode");
|
||||
if (scale_mode.type == TOML_STRING) {
|
||||
config->default_scale_mode = parse_scale_mode(scale_mode.u.s);
|
||||
}
|
||||
|
||||
// Parse filter_quality
|
||||
toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality");
|
||||
if (filter_quality.type == TOML_STRING) {
|
||||
config->default_filter_quality = parse_filter_quality(filter_quality.u.s);
|
||||
}
|
||||
|
||||
// Parse anchor
|
||||
toml_datum_t anchor = toml_seek(result.toptab, "anchor");
|
||||
if (anchor.type == TOML_STRING) {
|
||||
config->default_anchor = parse_anchor(anchor.u.s);
|
||||
}
|
||||
|
||||
// Parse anchor_x
|
||||
toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x");
|
||||
if (anchor_x.type == TOML_INT64) {
|
||||
config->default_anchor_x = (float)anchor_x.u.int64;
|
||||
} else if (anchor_x.type == TOML_FP64) {
|
||||
config->default_anchor_x = (float)anchor_x.u.fp64;
|
||||
}
|
||||
|
||||
// Parse anchor_y
|
||||
toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y");
|
||||
if (anchor_y.type == TOML_INT64) {
|
||||
config->default_anchor_y = (float)anchor_y.u.int64;
|
||||
} else if (anchor_y.type == TOML_FP64) {
|
||||
config->default_anchor_y = (float)anchor_y.u.fp64;
|
||||
}
|
||||
|
||||
// Parse downsampling section
|
||||
toml_datum_t downsampling = toml_seek(result.toptab, "downsampling");
|
||||
if (downsampling.type == TOML_TABLE) {
|
||||
toml_datum_t ds_tab = downsampling;
|
||||
|
||||
toml_datum_t enable = toml_seek(ds_tab, "enable");
|
||||
if (enable.type == TOML_BOOLEAN) {
|
||||
config->enable_downsampling = enable.u.boolean;
|
||||
}
|
||||
|
||||
toml_datum_t max_width = toml_seek(ds_tab, "max_output_width");
|
||||
if (max_width.type == TOML_INT64) {
|
||||
config->max_output_width = (int)max_width.u.int64;
|
||||
}
|
||||
|
||||
toml_datum_t max_height = toml_seek(ds_tab, "max_output_height");
|
||||
if (max_height.type == TOML_INT64) {
|
||||
config->max_output_height = (int)max_height.u.int64;
|
||||
}
|
||||
|
||||
toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor");
|
||||
if (min_scale.type == TOML_FP64) {
|
||||
config->min_scale_factor = (float)min_scale.u.fp64;
|
||||
} else if (min_scale.type == TOML_INT64) {
|
||||
config->min_scale_factor = (float)min_scale.u.int64;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse output mappings array
|
||||
toml_datum_t outputs = toml_seek(result.toptab, "output");
|
||||
if (outputs.type == TOML_ARRAY) {
|
||||
for (int i = 0; i < outputs.u.arr.size; i++) {
|
||||
toml_datum_t output = outputs.u.arr.elem[i];
|
||||
if (output.type != TOML_TABLE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
toml_datum_t output_tab = output;
|
||||
|
||||
// Get output name
|
||||
toml_datum_t name = toml_seek(output_tab, "name");
|
||||
if (name.type != TOML_STRING) {
|
||||
chroma_log("WARN", "Output mapping %d missing name", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get image path
|
||||
toml_datum_t image = toml_seek(output_tab, "image");
|
||||
if (image.type != TOML_STRING) {
|
||||
chroma_log("WARN", "Output mapping %d missing image", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
char *expanded_path = chroma_expand_path(image.u.s);
|
||||
const char *path_to_use = expanded_path ? expanded_path : image.u.s;
|
||||
|
||||
// Get optional settings with defaults from global config
|
||||
chroma_scale_mode_t out_scale = config->default_scale_mode;
|
||||
chroma_filter_quality_t out_filter = config->default_filter_quality;
|
||||
chroma_anchor_t out_anchor = config->default_anchor;
|
||||
float out_anchor_x = config->default_anchor_x;
|
||||
float out_anchor_y = config->default_anchor_y;
|
||||
|
||||
toml_datum_t out_scale_val = toml_seek(output_tab, "scale");
|
||||
if (out_scale_val.type == TOML_STRING) {
|
||||
out_scale = parse_scale_mode(out_scale_val.u.s);
|
||||
}
|
||||
|
||||
toml_datum_t out_filter_val = toml_seek(output_tab, "filter");
|
||||
if (out_filter_val.type == TOML_STRING) {
|
||||
out_filter = parse_filter_quality(out_filter_val.u.s);
|
||||
}
|
||||
|
||||
toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor");
|
||||
if (out_anchor_val.type == TOML_STRING) {
|
||||
out_anchor = parse_anchor(out_anchor_val.u.s);
|
||||
}
|
||||
|
||||
toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x");
|
||||
if (out_anchor_x_val.type == TOML_INT64) {
|
||||
out_anchor_x = (float)out_anchor_x_val.u.int64;
|
||||
} else if (out_anchor_x_val.type == TOML_FP64) {
|
||||
out_anchor_x = (float)out_anchor_x_val.u.fp64;
|
||||
}
|
||||
|
||||
toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y");
|
||||
if (out_anchor_y_val.type == TOML_INT64) {
|
||||
out_anchor_y = (float)out_anchor_y_val.u.int64;
|
||||
} else if (out_anchor_y_val.type == TOML_FP64) {
|
||||
out_anchor_y = (float)out_anchor_y_val.u.fp64;
|
||||
}
|
||||
|
||||
// Add the mapping
|
||||
if (add_output_mapping(config, name.u.s, path_to_use, out_scale,
|
||||
out_filter, out_anchor, out_anchor_x,
|
||||
out_anchor_y) != CHROMA_OK) {
|
||||
chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s);
|
||||
}
|
||||
|
||||
if (expanded_path) {
|
||||
free(expanded_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toml_free(result);
|
||||
|
||||
chroma_log("INFO",
|
||||
"Loaded TOML configuration: %d output mappings, default image: %s",
|
||||
config->mapping_count, config->default_image);
|
||||
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Initialize configuration with defaults
|
||||
static void init_default_config(chroma_config_t *config) {
|
||||
if (!config)
|
||||
|
|
@ -291,313 +434,12 @@ static void init_default_config(chroma_config_t *config) {
|
|||
config->max_output_height = 2160; // 4K height
|
||||
config->min_scale_factor = 0.25f; // don't scale below 25%
|
||||
|
||||
// Set default image path (can be overridden)
|
||||
const char *home = getenv("HOME");
|
||||
if (home) {
|
||||
snprintf(config->default_image, sizeof(config->default_image),
|
||||
"%s/.config/chroma/default.jpg", home);
|
||||
} else {
|
||||
strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
|
||||
}
|
||||
// Leave default_image empty - user must configure it explicitly
|
||||
// This avoids errors when the hardcoded path doesn't exist
|
||||
config->default_image[0] = '\0';
|
||||
}
|
||||
|
||||
// Parse a single configuration line
|
||||
static int parse_config_line(chroma_config_t *config, char *line,
|
||||
int line_number) {
|
||||
if (!config || !line) {
|
||||
return CHROMA_ERROR_INIT;
|
||||
}
|
||||
|
||||
// Skip empty lines and comments
|
||||
char *trimmed = trim_whitespace(line);
|
||||
if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Find the equals sign
|
||||
char *equals = strchr(trimmed, '=');
|
||||
if (!equals) {
|
||||
chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
|
||||
return CHROMA_OK; // continue parsing
|
||||
}
|
||||
|
||||
*equals = '\0';
|
||||
char *key = trim_whitespace(trimmed);
|
||||
char *value = trim_whitespace(equals + 1);
|
||||
|
||||
value = remove_quotes(value);
|
||||
|
||||
if (*key == '\0' || *value == '\0') {
|
||||
chroma_log("WARN", "Invalid config line %d: empty key or value",
|
||||
line_number);
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Parse configuration options
|
||||
if (strcasecmp(key, "default_image") == 0) {
|
||||
char *expanded_path = chroma_expand_path(value);
|
||||
const char *path_to_use = expanded_path ? expanded_path : value;
|
||||
size_t path_len = strlen(path_to_use);
|
||||
|
||||
if (path_len >= sizeof(config->default_image)) {
|
||||
chroma_log("ERROR", "Default image path too long: %s (max %zu)",
|
||||
path_to_use, sizeof(config->default_image) - 1);
|
||||
if (expanded_path) {
|
||||
free(expanded_path);
|
||||
}
|
||||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
|
||||
strcpy(config->default_image, path_to_use);
|
||||
|
||||
if (expanded_path) {
|
||||
chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
|
||||
chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
|
||||
path_len, expanded_path);
|
||||
free(expanded_path);
|
||||
} else {
|
||||
chroma_log("WARN", "Failed to expand path, using original: %s", value);
|
||||
}
|
||||
} else if (strcasecmp(key, "daemon") == 0 ||
|
||||
strcasecmp(key, "daemon_mode") == 0) {
|
||||
config->daemon_mode = parse_bool(value);
|
||||
chroma_log("DEBUG", "Set daemon mode: %s",
|
||||
config->daemon_mode ? "true" : "false");
|
||||
chroma_log("TRACE",
|
||||
"Daemon mode configuration: key='%s', value='%s', parsed=%s",
|
||||
key, value, config->daemon_mode ? "true" : "false");
|
||||
} else if (strcasecmp(key, "scale_mode") == 0 ||
|
||||
strcasecmp(key, "default_scale_mode") == 0) {
|
||||
config->default_scale_mode = parse_scale_mode(value);
|
||||
chroma_log("DEBUG", "Set default scale mode: %s",
|
||||
scale_mode_to_string(config->default_scale_mode));
|
||||
chroma_log("TRACE",
|
||||
"Scale mode configuration: key='%s', value='%s', parsed=%s", key,
|
||||
value, scale_mode_to_string(config->default_scale_mode));
|
||||
} else if (strcasecmp(key, "filter_quality") == 0 ||
|
||||
strcasecmp(key, "default_filter_quality") == 0) {
|
||||
config->default_filter_quality = parse_filter_quality(value);
|
||||
chroma_log("DEBUG", "Set default filter quality: %s",
|
||||
filter_quality_to_string(config->default_filter_quality));
|
||||
chroma_log("TRACE",
|
||||
"Filter quality configuration: key='%s', value='%s', parsed=%s",
|
||||
key, value,
|
||||
filter_quality_to_string(config->default_filter_quality));
|
||||
} else if (strcasecmp(key, "enable_downsampling") == 0) {
|
||||
config->enable_downsampling = parse_bool(value);
|
||||
chroma_log("DEBUG", "Set downsampling: %s",
|
||||
config->enable_downsampling ? "enabled" : "disabled");
|
||||
} else if (strcasecmp(key, "anchor") == 0) {
|
||||
config->default_anchor = parse_anchor(value);
|
||||
chroma_log("DEBUG", "Set default anchor: %s",
|
||||
anchor_to_string(config->default_anchor));
|
||||
} else if (strcasecmp(key, "anchor_x") == 0) {
|
||||
char *endptr = NULL;
|
||||
float ax = strtof(value, &endptr);
|
||||
if (endptr == value || *endptr != '\0') {
|
||||
chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)",
|
||||
value);
|
||||
config->default_anchor_x = 50.0f;
|
||||
} else if (ax < 0.0f || ax > 100.0f) {
|
||||
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value);
|
||||
config->default_anchor_x = 50.0f;
|
||||
} else {
|
||||
config->default_anchor_x = ax;
|
||||
chroma_log("DEBUG", "Set default anchor_x: %.1f", (double)ax);
|
||||
}
|
||||
} else if (strcasecmp(key, "anchor_y") == 0) {
|
||||
char *endptr = NULL;
|
||||
float ay = strtof(value, &endptr);
|
||||
if (endptr == value || *endptr != '\0') {
|
||||
chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)",
|
||||
value);
|
||||
config->default_anchor_y = 50.0f;
|
||||
} else if (ay < 0.0f || ay > 100.0f) {
|
||||
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value);
|
||||
config->default_anchor_y = 50.0f;
|
||||
} else {
|
||||
config->default_anchor_y = ay;
|
||||
chroma_log("DEBUG", "Set default anchor_y: %.1f", (double)ay);
|
||||
}
|
||||
} else if (strcasecmp(key, "max_output_width") == 0) {
|
||||
int width = atoi(value);
|
||||
if (width > 0 && width <= 16384) { // Reasonable limits
|
||||
config->max_output_width = width;
|
||||
chroma_log("DEBUG", "Set max output width: %d", width);
|
||||
} else {
|
||||
chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);
|
||||
}
|
||||
} else if (strcasecmp(key, "max_output_height") == 0) {
|
||||
int height = atoi(value);
|
||||
if (height > 0 && height <= 16384) { // Reasonable limits
|
||||
config->max_output_height = height;
|
||||
chroma_log("DEBUG", "Set max output height: %d", height);
|
||||
} else {
|
||||
chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);
|
||||
}
|
||||
} else if (strcasecmp(key, "min_scale_factor") == 0) {
|
||||
float factor = (float)atof(value);
|
||||
if (factor > 0.0f && factor <= 1.0f) {
|
||||
config->min_scale_factor = factor;
|
||||
chroma_log("DEBUG", "Set minimum scale factor: %.2f", (double)factor);
|
||||
} else {
|
||||
chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
|
||||
}
|
||||
} else if (strncasecmp(key, "output.", 7) == 0) {
|
||||
// Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
|
||||
const char *output_name = key + 7;
|
||||
if (*output_name == '\0') {
|
||||
chroma_log("WARN", "Invalid output mapping line %d: no output name",
|
||||
line_number);
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Check for extended output configuration with properties. Format:
|
||||
// output.DP-1.scale = fill
|
||||
// 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 if (strcasecmp(property, "anchor") == 0) {
|
||||
mapping->anchor = parse_anchor(value);
|
||||
// Set anchor_x/anchor_y based on named anchor
|
||||
switch (mapping->anchor) {
|
||||
case CHROMA_ANCHOR_TOP:
|
||||
mapping->anchor_x = 50.0f;
|
||||
mapping->anchor_y = 0.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_BOTTOM:
|
||||
mapping->anchor_x = 50.0f;
|
||||
mapping->anchor_y = 100.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_LEFT:
|
||||
mapping->anchor_x = 0.0f;
|
||||
mapping->anchor_y = 50.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_RIGHT:
|
||||
mapping->anchor_x = 100.0f;
|
||||
mapping->anchor_y = 50.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_TOP_LEFT:
|
||||
mapping->anchor_x = 0.0f;
|
||||
mapping->anchor_y = 0.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_TOP_RIGHT:
|
||||
mapping->anchor_x = 100.0f;
|
||||
mapping->anchor_y = 0.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_BOTTOM_LEFT:
|
||||
mapping->anchor_x = 0.0f;
|
||||
mapping->anchor_y = 100.0f;
|
||||
break;
|
||||
case CHROMA_ANCHOR_BOTTOM_RIGHT:
|
||||
mapping->anchor_x = 100.0f;
|
||||
mapping->anchor_y = 100.0f;
|
||||
break;
|
||||
default:
|
||||
mapping->anchor_x = 50.0f;
|
||||
mapping->anchor_y = 50.0f;
|
||||
break;
|
||||
}
|
||||
chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",
|
||||
output_name, anchor_to_string(mapping->anchor),
|
||||
(double)mapping->anchor_x, (double)mapping->anchor_y);
|
||||
} else if (strcasecmp(property, "anchor_x") == 0) {
|
||||
float ax = (float)atof(value);
|
||||
if (ax >= 0.0f && ax <= 100.0f) {
|
||||
mapping->anchor_x = ax;
|
||||
chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name,
|
||||
(double)ax);
|
||||
} else {
|
||||
mapping->anchor_x = 50.0f;
|
||||
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)",
|
||||
value);
|
||||
}
|
||||
} else if (strcasecmp(property, "anchor_y") == 0) {
|
||||
float ay = (float)atof(value);
|
||||
if (ay >= 0.0f && ay <= 100.0f) {
|
||||
mapping->anchor_y = ay;
|
||||
chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name,
|
||||
(double)ay);
|
||||
} else {
|
||||
mapping->anchor_y = 50.0f;
|
||||
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",
|
||||
value);
|
||||
}
|
||||
} 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, config->default_anchor,
|
||||
config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) {
|
||||
chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
|
||||
path_to_validate);
|
||||
if (expanded_path) {
|
||||
free(expanded_path);
|
||||
}
|
||||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
|
||||
if (expanded_path) {
|
||||
free(expanded_path);
|
||||
}
|
||||
} else {
|
||||
chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
|
||||
key);
|
||||
chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
|
||||
line_number, key, value);
|
||||
}
|
||||
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Load configuration from file
|
||||
// Load configuration from file (TOML format only)
|
||||
int chroma_config_load(chroma_config_t *config, const char *config_file) {
|
||||
if (!config) {
|
||||
return CHROMA_ERROR_INIT;
|
||||
|
|
@ -611,52 +453,20 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) {
|
|||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
FILE *file = fopen(config_file, "r");
|
||||
if (!file) {
|
||||
if (errno == ENOENT) {
|
||||
chroma_log("INFO", "Config file not found: %s (using defaults)",
|
||||
config_file);
|
||||
return CHROMA_OK;
|
||||
} else {
|
||||
chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
|
||||
strerror(errno));
|
||||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
// Check if file exists
|
||||
if (!chroma_path_exists(config_file)) {
|
||||
chroma_log("INFO", "Config file not found: %s (using defaults)",
|
||||
config_file);
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
chroma_log("INFO", "Loading configuration from: %s", config_file);
|
||||
chroma_log("TRACE",
|
||||
"Starting configuration parsing, estimated config size: %ld bytes",
|
||||
chroma_get_file_size(config_file));
|
||||
|
||||
char line[1024];
|
||||
int line_number = 0;
|
||||
int parse_errors = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), file)) {
|
||||
line_number++;
|
||||
|
||||
char *newline = strchr(line, '\n');
|
||||
if (newline) {
|
||||
*newline = '\0';
|
||||
}
|
||||
|
||||
if (parse_config_line(config, line, line_number) != CHROMA_OK) {
|
||||
parse_errors++;
|
||||
}
|
||||
// Load TOML configuration
|
||||
int result = chroma_config_load_toml(config, config_file);
|
||||
if (result != CHROMA_OK) {
|
||||
chroma_log("ERROR", "Failed to load TOML config from %s", config_file);
|
||||
return result;
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
|
||||
if (parse_errors > 0) {
|
||||
chroma_log("WARN", "Config file contained %d errors", parse_errors);
|
||||
// Continue anyway with partial configuration
|
||||
}
|
||||
|
||||
chroma_log("INFO",
|
||||
"Loaded configuration: %d output mappings, default image: %s",
|
||||
config->mapping_count, config->default_image);
|
||||
|
||||
// Log configuration memory usage
|
||||
size_t config_size =
|
||||
sizeof(chroma_config_t) +
|
||||
|
|
|
|||
19
src/core.c
19
src/core.c
|
|
@ -85,8 +85,19 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
|
|||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
|
||||
// Load or get cached image
|
||||
chroma_image_t *image = chroma_image_get_or_load(state, image_path);
|
||||
// Check if image path is empty (no default configured)
|
||||
if (strlen(image_path) == 0) {
|
||||
chroma_log("WARN",
|
||||
"No wallpaper image configured for output %u (%s). "
|
||||
"Set default_image in config or provide -c config.toml",
|
||||
output->id, output->name ? output->name : "unknown");
|
||||
return CHROMA_ERROR_CONFIG;
|
||||
}
|
||||
|
||||
// Load or get cached image with output dimensions for intelligent
|
||||
// downsampling
|
||||
chroma_image_t *image = chroma_image_get_or_load(
|
||||
state, image_path, output->width, output->height);
|
||||
if (!image) {
|
||||
chroma_log("ERROR", "Failed to load image for output %u: %s", output->id,
|
||||
image_path);
|
||||
|
|
@ -96,6 +107,7 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
|
|||
// Check if image changed and invalidate texture cache if neceessary
|
||||
bool image_changed = (output->image != image);
|
||||
if (image_changed && output->image) {
|
||||
chroma_image_release(output->image);
|
||||
chroma_output_invalidate_texture(output);
|
||||
output->vbo_dirty = true; // VBO needs update for new image
|
||||
chroma_log("DEBUG", "Image changed for output %u, invalidated texture",
|
||||
|
|
@ -161,7 +173,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
|
|||
// Render wallpaper
|
||||
int ret = chroma_render_wallpaper(state, output);
|
||||
if (ret != CHROMA_OK) {
|
||||
chroma_log("ERROR", "Failed to render wallpaper for output %u", output->id);
|
||||
chroma_log("ERROR", "Failed to render wallpaper for output %u: %s", output->id,
|
||||
chroma_error_string(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
|
|
|||
101
src/image.c
101
src/image.c
|
|
@ -58,8 +58,9 @@ static void calculate_optimal_size(int original_width, int original_height,
|
|||
// being*. Must be revisited in the future to see how it stands as the program
|
||||
// evolves.
|
||||
static int downsample_image(unsigned char *src_data, int src_width,
|
||||
int src_height, unsigned char *dst_data,
|
||||
int dst_width, int dst_height) {
|
||||
int src_height, int src_channels,
|
||||
unsigned char *dst_data, int dst_width,
|
||||
int dst_height, int dst_channels) {
|
||||
if (!src_data || !dst_data) {
|
||||
return -1;
|
||||
}
|
||||
|
|
@ -77,14 +78,18 @@ static int downsample_image(unsigned char *src_data, int src_width,
|
|||
src_x = (src_x >= src_width) ? src_width - 1 : src_x;
|
||||
src_y = (src_y >= src_height) ? src_height - 1 : src_y;
|
||||
|
||||
// Copy pixel (RGBA)
|
||||
int src_idx = (src_y * src_width + src_x) * 4;
|
||||
int dst_idx = (y * dst_width + x) * 4;
|
||||
// Copy pixel data
|
||||
int src_idx = (src_y * src_width + src_x) * src_channels;
|
||||
int dst_idx = (y * dst_width + x) * dst_channels;
|
||||
|
||||
dst_data[dst_idx + 0] = src_data[src_idx + 0]; // R
|
||||
dst_data[dst_idx + 1] = src_data[src_idx + 1]; // G
|
||||
dst_data[dst_idx + 2] = src_data[src_idx + 2]; // B
|
||||
dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A
|
||||
if (dst_channels == 4 && src_channels == 4) {
|
||||
dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A
|
||||
} else if (dst_channels == 4) {
|
||||
dst_data[dst_idx + 3] = 255; // Full alpha for RGB source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -92,8 +97,11 @@ static int downsample_image(unsigned char *src_data, int src_width,
|
|||
}
|
||||
|
||||
// Load image from file with configurable downsampling
|
||||
// output_width/output_height: actual output dimensions for intelligent
|
||||
// downsampling
|
||||
int chroma_image_load(chroma_image_t *image, const char *path,
|
||||
const chroma_config_t *config) {
|
||||
const chroma_config_t *config, int output_width,
|
||||
int output_height) {
|
||||
if (!image || !path) {
|
||||
chroma_log("ERROR", "Invalid parameters for image loading");
|
||||
return CHROMA_ERROR_INIT;
|
||||
|
|
@ -117,12 +125,24 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
(double)file_size / (1024.0 * 1024.0));
|
||||
}
|
||||
|
||||
// Load image data using stb_image, force RGBA format to avoid conversion
|
||||
// Load image data using stb_image
|
||||
// First, check actual channels to decide if we need alpha
|
||||
stbi_set_flip_vertically_on_load(0); // keep images right-side up
|
||||
|
||||
image->data =
|
||||
stbi_load(path, &image->width, &image->height, &image->channels, 4);
|
||||
image->channels = 4; // always RGBA after forced conversion
|
||||
int actual_channels = 0;
|
||||
if (!stbi_info(path, &image->width, &image->height, &actual_channels)) {
|
||||
chroma_log("ERROR", "Failed to get image info for %s: %s", path,
|
||||
stbi_failure_reason());
|
||||
return CHROMA_ERROR_IMAGE;
|
||||
}
|
||||
|
||||
// Load with actual channels or force RGBA if image has alpha
|
||||
// For wallpapers, we typically don't need alpha unless the image has it
|
||||
int desired_channels = (actual_channels == 4 || actual_channels == 2) ? 4 : 3;
|
||||
|
||||
image->data = stbi_load(path, &image->width, &image->height, &image->channels,
|
||||
desired_channels);
|
||||
image->channels = desired_channels;
|
||||
if (!image->data) {
|
||||
chroma_log("ERROR", "Failed to load image %s: %s", path,
|
||||
stbi_failure_reason());
|
||||
|
|
@ -137,13 +157,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
return CHROMA_ERROR_IMAGE;
|
||||
}
|
||||
|
||||
// Validate we have RGBA data (should always be true with forced conversion)
|
||||
if (image->channels != 4) {
|
||||
chroma_log("ERROR", "Failed to load image as RGBA: got %d channels",
|
||||
image->channels);
|
||||
chroma_image_free(image);
|
||||
return CHROMA_ERROR_IMAGE;
|
||||
}
|
||||
chroma_log("DEBUG", "Loaded image %s with %d channels (original had %d)",
|
||||
path, image->channels, actual_channels);
|
||||
|
||||
// Store original dimensions before potential downsampling
|
||||
int original_width = image->width;
|
||||
|
|
@ -155,9 +170,14 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
int optimal_height = original_height;
|
||||
|
||||
if (config && config->enable_downsampling) {
|
||||
calculate_optimal_size(original_width, original_height,
|
||||
config->max_output_width, config->max_output_height,
|
||||
&optimal_width, &optimal_height);
|
||||
// Use output dimensions if provided, otherwise fall back to config defaults
|
||||
int max_width =
|
||||
(output_width > 0) ? output_width : config->max_output_width;
|
||||
int max_height =
|
||||
(output_height > 0) ? output_height : config->max_output_height;
|
||||
|
||||
calculate_optimal_size(original_width, original_height, max_width,
|
||||
max_height, &optimal_width, &optimal_height);
|
||||
|
||||
// Apply minimum scale factor constraint
|
||||
float scale_x = (float)optimal_width / (float)original_width;
|
||||
|
|
@ -178,7 +198,7 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
(optimal_width < original_width || optimal_height < original_height);
|
||||
}
|
||||
|
||||
// Downsamp if needed and enabled
|
||||
// Downsample if needed and enabled
|
||||
if (should_downsample) {
|
||||
double reduction_ratio = (double)(optimal_width * optimal_height) /
|
||||
(double)(original_width * original_height) * 100.0;
|
||||
|
|
@ -187,7 +207,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
original_width, original_height, optimal_width, optimal_height,
|
||||
reduction_ratio);
|
||||
|
||||
size_t optimal_size = (size_t)optimal_width * (size_t)optimal_height * 4;
|
||||
size_t optimal_size =
|
||||
(size_t)optimal_width * (size_t)optimal_height * image->channels;
|
||||
unsigned char *downsampled_data = malloc(optimal_size);
|
||||
if (!downsampled_data) {
|
||||
chroma_log("ERROR", "Failed to allocate memory for downsampled image");
|
||||
|
|
@ -196,8 +217,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
}
|
||||
|
||||
if (downsample_image(image->data, original_width, original_height,
|
||||
downsampled_data, optimal_width,
|
||||
optimal_height) != 0) {
|
||||
image->channels, downsampled_data, optimal_width,
|
||||
optimal_height, image->channels) != 0) {
|
||||
chroma_log("ERROR", "Failed to downsample image");
|
||||
free(downsampled_data);
|
||||
chroma_image_free(image);
|
||||
|
|
@ -218,6 +239,7 @@ int chroma_image_load(chroma_image_t *image, const char *path,
|
|||
}
|
||||
|
||||
image->loaded = true;
|
||||
image->ref_count = 1; // Initial reference from the first output
|
||||
|
||||
// Calculate and log memory allocation
|
||||
size_t image_size =
|
||||
|
|
@ -258,6 +280,7 @@ void chroma_image_free(chroma_image_t *image) {
|
|||
image->height = 0;
|
||||
image->channels = 0;
|
||||
image->loaded = false;
|
||||
image->ref_count = 0;
|
||||
|
||||
if (strlen(image->path) > 0) {
|
||||
chroma_log("DEBUG", "Freed image: %s", image->path);
|
||||
|
|
@ -266,6 +289,18 @@ void chroma_image_free(chroma_image_t *image) {
|
|||
memset(image->path, 0, sizeof(image->path));
|
||||
}
|
||||
|
||||
// Release a reference to an image; free when ref_count reaches zero
|
||||
void chroma_image_release(chroma_image_t *image) {
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
image->ref_count--;
|
||||
if (image->ref_count <= 0) {
|
||||
chroma_image_free(image);
|
||||
}
|
||||
}
|
||||
|
||||
// Find image by path in state
|
||||
chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
|
||||
const char *path) {
|
||||
|
|
@ -283,8 +318,11 @@ chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
|
|||
}
|
||||
|
||||
// Load image if not already loaded
|
||||
// output_width/output_height: actual output dimensions for intelligent
|
||||
// downsampling
|
||||
chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
|
||||
const char *path) {
|
||||
const char *path, int output_width,
|
||||
int output_height) {
|
||||
if (!state || !path) {
|
||||
return NULL;
|
||||
}
|
||||
|
|
@ -292,7 +330,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
|
|||
// Check if already loaded
|
||||
chroma_image_t *existing = chroma_image_find_by_path(state, path);
|
||||
if (existing && existing->loaded) {
|
||||
chroma_log("DEBUG", "Using cached image: %s", path);
|
||||
chroma_log("DEBUG", "Using cached image: %s (ref_count: %d)", path,
|
||||
existing->ref_count);
|
||||
existing->ref_count++;
|
||||
return existing;
|
||||
}
|
||||
|
||||
|
|
@ -307,8 +347,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
|
|||
state->image_count++;
|
||||
}
|
||||
|
||||
// Load the image with configuration
|
||||
if (chroma_image_load(image, path, &state->config) != CHROMA_OK) {
|
||||
// Load the image with configuration and output dimensions
|
||||
if (chroma_image_load(image, path, &state->config, output_width,
|
||||
output_height) != CHROMA_OK) {
|
||||
// If this was a new slot, decrement count
|
||||
if (!existing) {
|
||||
state->image_count--;
|
||||
|
|
@ -376,7 +417,7 @@ void chroma_images_cleanup(chroma_state_t *state) {
|
|||
chroma_log("DEBUG", "Cleaning up %d images", state->image_count);
|
||||
|
||||
for (int i = 0; i < state->image_count; i++) {
|
||||
chroma_image_free(&state->images[i]);
|
||||
chroma_image_release(&state->images[i]);
|
||||
}
|
||||
|
||||
state->image_count = 0;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ static void print_usage(const char *program_name) {
|
|||
printf(" -h, --help Show this help\n");
|
||||
printf(" --version Show version information\n");
|
||||
printf("\nExamples:\n");
|
||||
printf(" %s -c ~/.config/chroma/chroma.conf\n", program_name);
|
||||
printf(" %s -c ~/.config/chroma/chroma.toml\n", program_name);
|
||||
printf(" %s --daemon\n", program_name);
|
||||
}
|
||||
|
||||
|
|
|
|||
149
src/render.c
149
src/render.c
|
|
@ -187,7 +187,6 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
|
|||
|
||||
// Vertex shader for simple texture rendering
|
||||
static const char *vertex_shader_source =
|
||||
"#version 120\n"
|
||||
"attribute vec2 position;\n"
|
||||
"attribute vec2 texcoord;\n"
|
||||
"varying vec2 v_texcoord;\n"
|
||||
|
|
@ -198,7 +197,7 @@ static const char *vertex_shader_source =
|
|||
|
||||
// Fragment shader for simple texture rendering
|
||||
static const char *fragment_shader_source =
|
||||
"#version 120\n"
|
||||
"precision mediump float;\n"
|
||||
"varying vec2 v_texcoord;\n"
|
||||
"uniform sampler2D texture;\n"
|
||||
"void main() {\n"
|
||||
|
|
@ -214,7 +213,7 @@ static const float vertices[] = {
|
|||
-1.0f, 1.0f, 0.0f, 0.0f, // top left
|
||||
};
|
||||
|
||||
static const unsigned int indices[] = {
|
||||
static const unsigned short indices[] = {
|
||||
0, 1, 2, // first triangle
|
||||
2, 3, 0 // second triangle
|
||||
};
|
||||
|
|
@ -284,17 +283,29 @@ static GLuint create_shader_program(void) {
|
|||
}
|
||||
|
||||
// Initialize OpenGL resources for output
|
||||
static int init_gl_resources(chroma_output_t *output) {
|
||||
static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) {
|
||||
if (!output || output->gl_resources_initialized) {
|
||||
return CHROMA_OK;
|
||||
}
|
||||
|
||||
// Create shader prog
|
||||
output->shader_program = create_shader_program();
|
||||
if (!output->shader_program) {
|
||||
chroma_log("ERROR", "Failed to create shader program for output %u",
|
||||
// Use shared shader program from state if available
|
||||
if (state && state->shader_program != 0) {
|
||||
output->shader_program = state->shader_program;
|
||||
chroma_log("DEBUG", "Using shared shader program for output %u",
|
||||
output->id);
|
||||
return CHROMA_ERROR_EGL;
|
||||
} else {
|
||||
// Create shader program
|
||||
output->shader_program = create_shader_program();
|
||||
if (!output->shader_program) {
|
||||
chroma_log("ERROR", "Failed to create shader program for output %u",
|
||||
output->id);
|
||||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
// Store in state for sharing
|
||||
if (state) {
|
||||
state->shader_program = output->shader_program;
|
||||
chroma_log("DEBUG", "Created shared shader program");
|
||||
}
|
||||
}
|
||||
|
||||
// Create and setup VBO/EBO
|
||||
|
|
@ -308,6 +319,10 @@ static int init_gl_resources(chroma_output_t *output) {
|
|||
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
|
||||
GL_STATIC_DRAW);
|
||||
|
||||
// Unbind buffers
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||
|
||||
output->texture_id = 0; // will be created when image is assigned
|
||||
output->vbo_dirty = true; // VBO needs initial update
|
||||
output->gl_resources_initialized = true;
|
||||
|
|
@ -370,9 +385,24 @@ static int update_texture_from_image(chroma_output_t *output,
|
|||
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,
|
||||
GL_RGBA, GL_UNSIGNED_BYTE, image->data);
|
||||
// Upload texture data with appropriate format based on channels
|
||||
GLint internal_format;
|
||||
GLenum format;
|
||||
if (image->channels == 4) {
|
||||
internal_format = GL_RGBA;
|
||||
format = GL_RGBA;
|
||||
} else if (image->channels == 3) {
|
||||
internal_format = GL_RGB;
|
||||
format = GL_RGB;
|
||||
} else if (image->channels == 2) {
|
||||
internal_format = GL_LUMINANCE_ALPHA;
|
||||
format = GL_LUMINANCE_ALPHA;
|
||||
} else {
|
||||
internal_format = GL_LUMINANCE;
|
||||
format = GL_LUMINANCE;
|
||||
}
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height,
|
||||
0, format, GL_UNSIGNED_BYTE, image->data);
|
||||
|
||||
// Generate mipmaps for trilinear filtering if they're needed
|
||||
if (needs_mipmaps) {
|
||||
|
|
@ -385,38 +415,24 @@ static int update_texture_from_image(chroma_output_t *output,
|
|||
// Mark this output as having uploaded its texture
|
||||
output->texture_uploaded = true;
|
||||
|
||||
// Free system RAM copy only when ALL outputs using this image have uploaded
|
||||
// to GPU
|
||||
if (image->data) {
|
||||
// Count total outputs using this image and how many have uploaded
|
||||
int total_using = 0;
|
||||
int uploaded_count = 0;
|
||||
|
||||
chroma_state_t *state = output->state;
|
||||
for (int i = 0; i < state->output_count; i++) {
|
||||
if (state->outputs[i].active && state->outputs[i].image == image) {
|
||||
total_using++;
|
||||
if (state->outputs[i].texture_uploaded) {
|
||||
uploaded_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only free image data when ALL outputs using it have uploaded
|
||||
if (total_using > 0 && uploaded_count >= total_using) {
|
||||
size_t freed_bytes = (size_t)image->width * (size_t)image->height *
|
||||
(size_t)image->channels;
|
||||
stbi_image_free(image->data);
|
||||
image->data = NULL;
|
||||
chroma_log("INFO",
|
||||
"Freed %.2f MB of image data after all %d outputs uploaded to "
|
||||
"GPU: %s",
|
||||
(double)freed_bytes / (1024.0 * 1024.0), total_using,
|
||||
image->path);
|
||||
chroma_log_resource_deallocation("image_data", freed_bytes,
|
||||
"post-gpu-upload");
|
||||
chroma_log_memory_stats("post-gpu-upload");
|
||||
}
|
||||
// Decrement reference count and free system RAM when all outputs
|
||||
// have uploaded their textures
|
||||
image->ref_count--;
|
||||
if (image->ref_count <= 0 && image->data) {
|
||||
size_t freed_bytes =
|
||||
(size_t)image->width * (size_t)image->height * (size_t)image->channels;
|
||||
stbi_image_free(image->data);
|
||||
image->data = NULL;
|
||||
chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s",
|
||||
(double)freed_bytes / (1024.0 * 1024.0), image->path);
|
||||
chroma_log_resource_deallocation("image_data", freed_bytes,
|
||||
"post-gpu-upload");
|
||||
chroma_log_memory_stats("post-gpu-upload");
|
||||
} else {
|
||||
chroma_log("DEBUG",
|
||||
"Keeping image data for %s (ref_count: %d, waiting for %d more "
|
||||
"outputs)",
|
||||
image->path, image->ref_count, image->ref_count);
|
||||
}
|
||||
|
||||
chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
|
||||
|
|
@ -425,7 +441,8 @@ static int update_texture_from_image(chroma_output_t *output,
|
|||
}
|
||||
|
||||
// Cleanup OpenGL resources for output
|
||||
static void cleanup_gl_resources(chroma_output_t *output) {
|
||||
static void cleanup_gl_resources(chroma_state_t *state,
|
||||
chroma_output_t *output) {
|
||||
if (!output || !output->gl_resources_initialized) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -436,8 +453,12 @@ static void cleanup_gl_resources(chroma_output_t *output) {
|
|||
output->texture_id = 0;
|
||||
}
|
||||
|
||||
// Don't delete shared shader program here, it's managed by state
|
||||
if (output->shader_program != 0) {
|
||||
glDeleteProgram(output->shader_program);
|
||||
// Only delete if this output owns the program (not shared)
|
||||
if (state && state->shader_program != output->shader_program) {
|
||||
glDeleteProgram(output->shader_program);
|
||||
}
|
||||
output->shader_program = 0;
|
||||
}
|
||||
|
||||
|
|
@ -466,9 +487,9 @@ static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
|
|||
EGL_BLUE_SIZE,
|
||||
8,
|
||||
EGL_ALPHA_SIZE,
|
||||
8,
|
||||
0,
|
||||
EGL_RENDERABLE_TYPE,
|
||||
EGL_OPENGL_BIT,
|
||||
EGL_OPENGL_ES2_BIT,
|
||||
EGL_NONE};
|
||||
|
||||
EGLint num_configs;
|
||||
|
|
@ -508,9 +529,9 @@ int chroma_egl_init(chroma_state_t *state) {
|
|||
chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
|
||||
chroma_log_memory_stats("post-egl-init");
|
||||
|
||||
// Bind OpenGL API
|
||||
if (!eglBindAPI(EGL_OPENGL_API)) {
|
||||
chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
|
||||
// Bind OpenGL ES API for better efficiency on embedded/GPU drivers
|
||||
if (!eglBindAPI(EGL_OPENGL_ES_API)) {
|
||||
chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError());
|
||||
chroma_egl_cleanup(state);
|
||||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
|
|
@ -521,9 +542,8 @@ int chroma_egl_init(chroma_state_t *state) {
|
|||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
|
||||
// Create EGL context
|
||||
EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
|
||||
EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
|
||||
// Create EGL context for GLES 2.0
|
||||
EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
|
||||
|
||||
state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
|
||||
EGL_NO_CONTEXT, context_attributes);
|
||||
|
|
@ -543,6 +563,12 @@ void chroma_egl_cleanup(chroma_state_t *state) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clean up shared shader program while context is still current
|
||||
if (state->shader_program != 0) {
|
||||
glDeleteProgram(state->shader_program);
|
||||
state->shader_program = 0;
|
||||
}
|
||||
|
||||
if (state->egl_display != EGL_NO_DISPLAY) {
|
||||
eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
|
||||
EGL_NO_CONTEXT);
|
||||
|
|
@ -644,8 +670,8 @@ void chroma_surface_destroy(chroma_output_t *output) {
|
|||
return;
|
||||
}
|
||||
|
||||
// Clean up OpenGL resources first
|
||||
cleanup_gl_resources(output);
|
||||
// Clean up OpenGL resources first. We use output's back-reference to state
|
||||
cleanup_gl_resources(output->state, output);
|
||||
|
||||
if (output->egl_surface != EGL_NO_SURFACE) {
|
||||
eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
|
||||
|
|
@ -667,6 +693,8 @@ void chroma_surface_destroy(chroma_output_t *output) {
|
|||
output->surface = NULL;
|
||||
}
|
||||
|
||||
output->configured = false;
|
||||
|
||||
// Log surface destruction
|
||||
size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
|
||||
chroma_log_resource_deallocation("egl_surface", surface_size,
|
||||
|
|
@ -689,7 +717,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
|
|||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
|
||||
if (init_gl_resources(output) != CHROMA_OK) {
|
||||
if (init_gl_resources(state, output) != CHROMA_OK) {
|
||||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
|
||||
|
|
@ -762,7 +790,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
|
|||
4 * sizeof(float), (void *)(2 * sizeof(float)));
|
||||
|
||||
// Draw
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
|
||||
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
|
||||
|
||||
// Unbind resources
|
||||
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||
|
|
@ -770,16 +798,13 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
|
|||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
glUseProgram(0);
|
||||
|
||||
// Swap buffers
|
||||
// Swap buffers; this implicitly commits the surface for EGL on Wayland
|
||||
if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
|
||||
chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
|
||||
output->id, eglGetError());
|
||||
return CHROMA_ERROR_EGL;
|
||||
}
|
||||
|
||||
// Commit surface
|
||||
wl_surface_commit(output->surface);
|
||||
|
||||
chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
|
||||
output->id);
|
||||
return CHROMA_OK;
|
||||
|
|
|
|||
|
|
@ -91,9 +91,8 @@ static void layer_surface_configure(void *data,
|
|||
chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u",
|
||||
output->id, serial);
|
||||
|
||||
// Commit the surface to apply the acknowledgment
|
||||
wl_surface_commit(output->surface);
|
||||
chroma_log("TRACE", "Surface committed for output %u", output->id);
|
||||
// Mark as configured - actual commit happens in render via eglSwapBuffers
|
||||
output->configured = true;
|
||||
|
||||
chroma_log("DEBUG", "Acknowledged layer surface configure for output %u",
|
||||
output->id);
|
||||
|
|
@ -382,6 +381,12 @@ void chroma_output_remove(chroma_state_t *state, uint32_t id) {
|
|||
chroma_log("INFO", "Removing output %u (%s)", id,
|
||||
output->name ? output->name : "unknown");
|
||||
|
||||
// Release image reference if the output holds one
|
||||
if (output->image) {
|
||||
chroma_image_release(output->image);
|
||||
output->image = NULL;
|
||||
}
|
||||
|
||||
// Clean up surface if it exists
|
||||
if (output->surface) {
|
||||
chroma_surface_destroy(output);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue