brewery
notashelf /
172c36ba42b26c85d310f1e55773d63642061ada

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit 172c36ba42b2

tarball

NotAShelf <raf@notashelf.dev> · 2026-06-19 16:54 UTC

unverified3 files changed+151-14
config: parse new configuration sections

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic750cc9df5d7f837d27776c1e3b9736b6a6a6964
diff --git a/chroma.toml.sample b/chroma.toml.sampleindex fa0fb90..33eda55 100644--- a/chroma.toml.sample+++ b/chroma.toml.sample@@ -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.diff --git a/src/config.c b/src/config.cindex c308904..82b447b 100644--- a/src/config.c+++ b/src/config.c@@ -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++) {diff --git a/src/core.c b/src/core.cindex 4b3a75a..b490a5e 100644--- a/src/core.c+++ b/src/core.c@@ -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);     }   }