notashelf /
172c36ba42b26c85d310f1e55773d63642061ada
chroma
publicLightweight wallpaper daemon for Wayland
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.gitCommit 172c36ba42b2
tarballunverified3 files changed+151-14
@@ -14,9 +14,16 @@ # Default wallpaper for outputs without specific mapping # Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM+# Also supported (with optional libraries): farbfeld, SVG, WebP, TIFF, AVIF, JPEG XL # Paths can be absolute or relative, ~ expansion is supported. default_image = "~/.config/chroma/default.jpg" +# Solid color background (overrides default_image when set)+# Format: RRGGBB or RRGGBBAA (hex, no # prefix)+# Example: solid_color = "1a1b26" (Tokyo Night background)+# Leave empty to use image-based wallpapers+# solid_color = ""+ # Whether to run as a background daemon # Usually set via command line option --daemon, but can be set here too daemon_mode = false@@ -63,6 +70,27 @@ max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p) # Range: 0.1 to 1.0 (10% to 100%) min_scale_factor = 0.25 # Don't scale below 25% of original size +# Smooth crossfade transitions when switching wallpapers+[transition]+enable = false # Enable smooth crossfade transitions+duration_ms = 300 # Transition duration in milliseconds++# Animated wallpaper support (GIF playback)+[animation]+enable = true # Enable animated GIF playback+max_fps = 30 # Maximum animation frame rate++# IPC socket for runtime wallpaper control+# When enabled, chroma listens on a Unix domain socket for commands:+# echo "set DP-1 ~/wallpaper.jpg" | nc -U /tmp/chroma.sock+# echo "set_all ~/wallpaper.jpg" | nc -U /tmp/chroma.sock+# echo "clear rrggbb" | nc -U /tmp/chroma.sock+# echo "reload" | nc -U /tmp/chroma.sock+# echo "ping" | nc -U /tmp/chroma.sock+[ipc]+enable = false+socket_path = "/tmp/chroma.sock"+ # Output-specific wallpaper mappings # ================================== # Each [[output]] block defines a mapping for a specific output.@@ -1,11 +1,9 @@-#include <ctype.h>-#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> -#include "chroma.h"+#include "../include/chroma.h" #include "vendor/tomlc17.h" // Match output name/description against a config pattern@@ -263,6 +261,60 @@ int chroma_config_load_toml(chroma_config_t *config, const char *config_file) { config->daemon_mode = daemon_mode.u.boolean; } + // Parse solid_color (optional global background color)+ toml_datum_t solid_color = toml_seek(result.toptab, "solid_color");+ if (solid_color.type == TOML_STRING) {+ strncpy(config->solid_color, solid_color.u.s,+ sizeof(config->solid_color) - 1);+ }++ // Parse transition section+ toml_datum_t transition = toml_seek(result.toptab, "transition");+ if (transition.type == TOML_TABLE) {+ toml_datum_t t_enable = toml_seek(transition, "enable");+ if (t_enable.type == TOML_BOOLEAN) {+ config->transition_enabled = t_enable.u.boolean;+ }+ toml_datum_t t_duration = toml_seek(transition, "duration_ms");+ if (t_duration.type == TOML_INT64) {+ config->transition_duration_ms = (int)t_duration.u.int64;+ }+ }++ // Parse animation section+ toml_datum_t animation = toml_seek(result.toptab, "animation");+ if (animation.type == TOML_TABLE) {+ toml_datum_t a_enable = toml_seek(animation, "enable");+ if (a_enable.type == TOML_BOOLEAN) {+ config->animation_enabled = a_enable.u.boolean;+ }+ toml_datum_t a_fps = toml_seek(animation, "max_fps");+ if (a_fps.type == TOML_INT64) {+ config->animation_max_fps = (int)a_fps.u.int64;+ }+ }++ // Parse IPC section+ toml_datum_t ipc = toml_seek(result.toptab, "ipc");+ if (ipc.type == TOML_TABLE) {+ toml_datum_t i_enable = toml_seek(ipc, "enable");+ if (i_enable.type == TOML_BOOLEAN) {+ config->ipc_enabled = i_enable.u.boolean;+ }+ toml_datum_t i_socket = toml_seek(ipc, "socket_path");+ if (i_socket.type == TOML_STRING) {+ char *expanded = chroma_expand_path(i_socket.u.s);+ if (expanded) {+ strncpy(config->ipc_socket_path, expanded,+ sizeof(config->ipc_socket_path) - 1);+ free(expanded);+ } else {+ strncpy(config->ipc_socket_path, i_socket.u.s,+ sizeof(config->ipc_socket_path) - 1);+ }+ }+ }+ // Parse scale_mode toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode"); if (scale_mode.type == TOML_STRING) {@@ -434,6 +486,15 @@ 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% + // Feature defaults+ config->solid_color[0] = '\0';+ config->transition_enabled = false;+ config->transition_duration_ms = 300;+ config->animation_enabled = true;+ config->animation_max_fps = 30;+ config->ipc_enabled = false;+ config->ipc_socket_path[0] = '\0';+ // Leave default_image empty - user must configure it explicitly // This avoids errors when the hardcoded path doesn't exist config->default_image[0] = '\0';@@ -600,6 +661,18 @@ void chroma_config_print(const chroma_config_t *config) { chroma_log("INFO", "Min scale factor: %.2f", (double)config->min_scale_factor); }+ if (config->solid_color[0] != '\0') {+ chroma_log("INFO", "Solid color: %s", config->solid_color);+ }+ chroma_log("INFO", "Transitions: %s (%dms)",+ config->transition_enabled ? "enabled" : "disabled",+ config->transition_duration_ms);+ chroma_log("INFO", "Animations: %s (max %d fps)",+ config->animation_enabled ? "enabled" : "disabled",+ config->animation_max_fps);+ chroma_log("INFO", "IPC: %s (%s)",+ config->ipc_enabled ? "enabled" : "disabled",+ config->ipc_enabled ? config->ipc_socket_path : "n/a"); chroma_log("INFO", "Output mappings: %d", config->mapping_count); for (int i = 0; i < config->mapping_count; i++) {@@ -20,6 +20,7 @@ int chroma_init(chroma_state_t *state) { // Initialize all fields to zero memset(state, 0, sizeof(chroma_state_t));+ state->ipc_fd = -1; // Set initial state state->running = false;@@ -258,6 +259,10 @@ int chroma_run(chroma_state_t *state) { chroma_log("INFO", "Performing initial wallpaper assignment"); assign_wallpapers_to_all_outputs(state); + int ipc_fd = chroma_ipc_get_fd(state);+ bool has_anim = chroma_anim_needs_redraw(state);+ bool has_transition = chroma_transition_needs_redraw(state);+ // Main event loop while (state->running && !chroma_should_quit) { // Dispatch any pending events first@@ -267,6 +272,18 @@ int chroma_run(chroma_state_t *state) { break; } + // Handle animation frame updates+ if (has_anim) {+ chroma_anim_update_outputs(state);+ has_anim = chroma_anim_needs_redraw(state);+ }++ // Handle transition frame updates+ if (has_transition) {+ chroma_transition_update_outputs(state);+ has_transition = chroma_transition_needs_redraw(state);+ }+ // Prepare to read events if (wl_display_prepare_read(state->display) == -1) { chroma_log("ERROR", "Failed to prepare Wayland display for reading");@@ -289,21 +306,32 @@ int chroma_run(chroma_state_t *state) { break; } - // Use `select()` to wait for events with longer timeout to reduce CPU usage+ // Build fd_set for select fd_set readfds;- struct timeval timeout;- FD_ZERO(&readfds); FD_SET(fd, &readfds); - timeout.tv_sec = 10;- timeout.tv_usec = 0;+ int max_fd = fd;+ if (ipc_fd >= 0) {+ FD_SET(ipc_fd, &readfds);+ if (ipc_fd > max_fd)+ max_fd = ipc_fd;+ } - int select_result = select(fd + 1, &readfds, NULL, NULL, &timeout);+ // Use a short timeout when animations or transitions are active+ struct timeval timeout;+ if (has_anim || has_transition) {+ timeout.tv_sec = 0;+ timeout.tv_usec = 16000; // ~60fps+ } else {+ timeout.tv_sec = 10;+ timeout.tv_usec = 0;+ }++ int select_result = select(max_fd + 1, &readfds, NULL, NULL, &timeout); if (select_result == -1) { if (errno == EINTR) {- // Interrupted by signal, check if we should quit wl_display_cancel_read(state->display); continue; }@@ -313,17 +341,25 @@ int chroma_run(chroma_state_t *state) { } if (select_result == 0) {- // Timeout - no events available+ // Timeout - check for animation/transition needs wl_display_cancel_read(state->display);+ has_anim = chroma_anim_needs_redraw(state);+ has_transition = chroma_transition_needs_redraw(state); continue; } - // Events are available- if (FD_ISSET(fd, &readfds)) {+ // Handle IPC events+ if (ipc_fd >= 0 && FD_ISSET(ipc_fd, &readfds)) {+ chroma_ipc_process(state);+ select_result--;+ }++ // Handle Wayland events+ if (select_result > 0 && FD_ISSET(fd, &readfds)) { if (process_wayland_events(state) != CHROMA_OK) { break; }- } else {+ } else if (select_result > 0) { wl_display_cancel_read(state->display); } }