brewery
notashelf /
292491b7388d7799a4e5ed0ddeb0265872b2f9d2

chroma

public

Lightweight wallpaper daemon for Wayland

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

Commit 292491b7388d

tarball

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

unverified2 files changed+554-0
ipc: add unix socket for runtime commands

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I87686ce45b37d39c938b84a243634e9e6a6a6964
diff --git a/src/ipc.c b/src/ipc.cnew file mode 100644index 0000000..4206fd9--- /dev/null+++ b/src/ipc.c@@ -0,0 +1,545 @@+#include <errno.h>+#include <fcntl.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <sys/socket.h>+#include <sys/stat.h>+#include <sys/un.h>+#include <unistd.h>++#include "../include/chroma.h"++#define IPC_MAX_MSG 4096+#define IPC_RESP_OK "ok\n"+#define IPC_RESP_ERR "err %s\n"++static int parse_set_command(const char *cmd, char *output_name, size_t name_sz,+                             char *image_path, size_t path_sz) {+  char action[64];+  int n = sscanf(cmd, "%63s", action);+  if (n != 1)+    return -1;++  if (strcmp(action, "set") == 0) {+    const char *rest = cmd + 4;+    while (*rest == ' ')+      rest++;++    const char *name_start = rest;+    while (*rest && *rest != ' ')+      rest++;+    size_t name_len = (size_t)(rest - name_start);+    if (name_len == 0 || name_len >= name_sz)+      return -1;+    memcpy(output_name, name_start, name_len);+    output_name[name_len] = '\0';++    while (*rest == ' ')+      rest++;+    const char *path_start = rest;+    while (*rest && *rest != '\n' && *rest != '\r')+      rest++;+    size_t path_len = (size_t)(rest - path_start);+    if (path_len == 0 || path_len >= path_sz)+      return -1;+    memcpy(image_path, path_start, path_len);+    image_path[path_len] = '\0';+    return 0;+  }++  return -1;+}++static int parse_set_all_command(const char *cmd, char *image_path,+                                 size_t path_sz) {+  char action[64];+  int n = sscanf(cmd, "%63s", action);+  if (n != 1)+    return -1;++  if (strcmp(action, "set_all") == 0) {+    const char *rest = cmd + 8;+    while (*rest == ' ')+      rest++;+    const char *path_start = rest;+    while (*rest && *rest != '\n' && *rest != '\r')+      rest++;+    size_t path_len = (size_t)(rest - path_start);+    if (path_len == 0 || path_len >= path_sz)+      return -1;+    memcpy(image_path, path_start, path_len);+    image_path[path_len] = '\0';+    return 0;+  }++  return -1;+}++static int parse_clear_command(const char *cmd, char *output_name,+                               size_t name_sz, char *hex_color,+                               size_t color_sz) {+  (void)name_sz;+  char action[64];+  int n = sscanf(cmd, "%63s", action);+  if (n != 1)+    return -1;++  if (strcmp(action, "clear") == 0) {+    const char *rest = cmd + 6;+    while (*rest == ' ')+      rest++;++    if (*rest == '\0' || *rest == '\n' || *rest == '\r') {+      output_name[0] = '\0';+      hex_color[0] = '\0';+      return 0;+    }++    const char *arg1_start = rest;+    while (*rest && *rest != ' ')+      rest++;+    size_t arg1_len = (size_t)(rest - arg1_start);++    while (*rest == ' ')+      rest++;++    if (*rest == '\0' || *rest == '\n' || *rest == '\r') {+      memcpy(hex_color, arg1_start, arg1_len);+      hex_color[arg1_len] = '\0';+      output_name[0] = '\0';+      return 0;+    }++    memcpy(output_name, arg1_start, arg1_len);+    output_name[arg1_len] = '\0';++    const char *color_start = rest;+    while (*rest && *rest != '\n' && *rest != '\r')+      rest++;+    size_t color_len = (size_t)(rest - color_start);+    if (color_len >= color_sz)+      return -1;+    memcpy(hex_color, color_start, color_len);+    hex_color[color_len] = '\0';+    return 0;+  }++  return -1;+}++static uint32_t parse_hex_color(const char *hex, bool *valid) {+  *valid = false;+  if (!hex || hex[0] == '\0')+    return 0xFF000000;++  size_t len = strlen(hex);+  if (len != 6 && len != 8)+    return 0xFF000000;++  for (size_t i = 0; i < len; i++) {+    if (!((hex[i] >= '0' && hex[i] <= '9') ||+          (hex[i] >= 'a' && hex[i] <= 'f') || (hex[i] >= 'A' && hex[i] <= 'F')))+      return 0xFF000000;+  }++  unsigned int r, g, b, a = 0xFF;+  if (len == 6) {+    sscanf(hex, "%2x%2x%2x", &r, &g, &b);+  } else {+    sscanf(hex, "%2x%2x%2x%2x", &r, &g, &b, &a);+  }++  *valid = true;+  return ((uint32_t)a << 24) | ((uint32_t)r << 16) | ((uint32_t)g << 8) |+         (uint32_t)b;+}++static int send_response(int fd, const char *response) {+  size_t len = strlen(response);+  ssize_t written = write(fd, response, len);+  return (written == (ssize_t)len) ? 0 : -1;+}++static int handle_command(chroma_state_t *state, int client_fd,+                          const char *command) {+  chroma_log("DEBUG", "IPC command: %s", command);++  if (strcmp(command, "ping") == 0) {+    return send_response(client_fd, "pong\n");+  }++  if (strcmp(command, "reload") == 0) {+    const char *cfg = state->config_file[0] != '\0' ? state->config_file : NULL;+    int ret = chroma_reload_config(state, cfg);+    if (ret != CHROMA_OK) {+      return send_response(client_fd, "err config_reload_failed\n");+    }+    return send_response(client_fd, IPC_RESP_OK);+  }++  if (strncmp(command, "set_all ", 8) == 0 || strcmp(command, "set_all") == 0) {+    char image_path[MAX_PATH_LEN];+    if (parse_set_all_command(command, image_path, sizeof(image_path)) != 0) {+      return send_response(client_fd, "err invalid_syntax\n");+    }+    int ret = chroma_set_wallpaper_for_all(state, image_path);+    if (ret != CHROMA_OK) {+      return send_response(client_fd, "err failed_to_set\n");+    }+    return send_response(client_fd, IPC_RESP_OK);+  }++  if (strncmp(command, "set ", 4) == 0) {+    char output_name[256];+    char image_path[MAX_PATH_LEN];+    if (parse_set_command(command, output_name, sizeof(output_name), image_path,+                          sizeof(image_path)) != 0) {+      return send_response(client_fd, "err invalid_syntax\n");+    }+    int ret = chroma_set_wallpaper_for_output(state, output_name, image_path);+    if (ret != CHROMA_OK) {+      return send_response(client_fd, "err failed_to_set\n");+    }+    return send_response(client_fd, IPC_RESP_OK);+  }++  if (strncmp(command, "clear", 5) == 0) {+    char output_name[256];+    char hex_color[10];+    if (parse_clear_command(command, output_name, sizeof(output_name),+                            hex_color, sizeof(hex_color)) != 0) {+      return send_response(client_fd, "err invalid_syntax\n");+    }+    if (output_name[0] != '\0') {+      int ret =+          chroma_set_solid_color_for_output(state, output_name, hex_color);+      if (ret != CHROMA_OK) {+        return send_response(client_fd, "err failed_to_set\n");+      }+    } else {+      int ret = chroma_set_solid_color_for_all(state, hex_color);+      if (ret != CHROMA_OK) {+        return send_response(client_fd, "err failed_to_set\n");+      }+    }+    return send_response(client_fd, IPC_RESP_OK);+  }++  return send_response(client_fd, "err unknown_command\n");+}++int chroma_ipc_init(chroma_state_t *state) {+  if (!state || !state->config.ipc_enabled) {+    return CHROMA_OK;+  }++  const char *socket_path = state->config.ipc_socket_path;+  if (socket_path[0] == '\0') {+    chroma_log("WARN", "IPC enabled but no socket path configured");+    return CHROMA_OK;+  }++  struct sockaddr_un addr;+  memset(&addr, 0, sizeof(addr));+  addr.sun_family = AF_UNIX;++  size_t path_len = strlen(socket_path);+  if (path_len >= sizeof(addr.sun_path)) {+    chroma_log("ERROR", "IPC socket path too long (%zu >= %zu)", path_len,+               sizeof(addr.sun_path));+    return CHROMA_ERROR_CONFIG;+  }+  memcpy(addr.sun_path, socket_path, path_len);++  struct stat st;+  if (lstat(socket_path, &st) == 0) {+    if (!S_ISSOCK(st.st_mode)) {+      chroma_log("ERROR", "IPC socket path exists but is not a socket: %s",+                 socket_path);+      return CHROMA_ERROR_CONFIG;+    }+    unlink(socket_path);+  }++  int fd = socket(AF_UNIX, SOCK_STREAM, 0);+  if (fd < 0) {+    chroma_log("ERROR", "Failed to create IPC socket: %s", strerror(errno));+    return CHROMA_ERROR_INIT;+  }++  if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {+    chroma_log("ERROR", "Failed to bind IPC socket to %s: %s", socket_path,+               strerror(errno));+    close(fd);+    return CHROMA_ERROR_INIT;+  }++  if (listen(fd, 5) < 0) {+    chroma_log("ERROR", "Failed to listen on IPC socket: %s", strerror(errno));+    close(fd);+    return CHROMA_ERROR_INIT;+  }++  state->ipc_fd = fd;+  chroma_log("INFO", "IPC socket listening on %s", socket_path);+  return CHROMA_OK;+}++void chroma_ipc_cleanup(chroma_state_t *state) {+  if (!state || state->ipc_fd < 0) {+    return;+  }++  close(state->ipc_fd);+  state->ipc_fd = -1;++  if (state->config.ipc_socket_path[0] != '\0') {+    unlink(state->config.ipc_socket_path);+  }++  chroma_log("DEBUG", "IPC socket cleaned up");+}++int chroma_ipc_get_fd(chroma_state_t *state) {+  if (!state || state->ipc_fd < 0) {+    return -1;+  }+  return state->ipc_fd;+}++int chroma_ipc_process(chroma_state_t *state) {+  if (!state || state->ipc_fd < 0) {+    return CHROMA_OK;+  }++  int client_fd = accept(state->ipc_fd, NULL, NULL);+  if (client_fd < 0) {+    if (errno == EAGAIN || errno == EWOULDBLOCK) {+      return CHROMA_OK;+    }+    chroma_log("ERROR", "IPC accept failed: %s", strerror(errno));+    return CHROMA_OK;+  }++  char buffer[IPC_MAX_MSG];+  memset(buffer, 0, sizeof(buffer));++  ssize_t n = read(client_fd, buffer, sizeof(buffer) - 1);+  if (n <= 0) {+    close(client_fd);+    return CHROMA_OK;+  }++  buffer[n] = '\0';++  size_t len = strlen(buffer);+  while (len > 0 && (buffer[len - 1] == '\n' || buffer[len - 1] == '\r')) {+    buffer[--len] = '\0';+  }++  handle_command(state, client_fd, buffer);++  close(client_fd);+  return CHROMA_OK;+}++int chroma_set_wallpaper_for_output(chroma_state_t *state,+                                    const char *output_name,+                                    const char *image_path) {+  if (!state || !output_name || !image_path) {+    return CHROMA_ERROR_INIT;+  }++  char *expanded = chroma_expand_path(image_path);+  const char *path = expanded ? expanded : image_path;++  if (!chroma_path_exists(path)) {+    chroma_log("ERROR", "Image not found: %s", path);+    if (expanded)+      free(expanded);+    return CHROMA_ERROR_IMAGE;+  }++  chroma_output_t *output = chroma_output_find_by_name(state, output_name);+  if (!output) {+    chroma_log("ERROR", "Output not found: %s", output_name);+    if (expanded)+      free(expanded);+    return CHROMA_ERROR_INIT;+  }++  chroma_image_t *image =+      chroma_image_get_or_load(state, path, output->width, output->height);+  if (!image) {+    chroma_log("ERROR", "Failed to load image for output %s: %s", output_name,+               path);+    if (expanded)+      free(expanded);+    return CHROMA_ERROR_IMAGE;+  }++  if (output->image) {+    chroma_image_release(output->image);+  }+  output->image = image;++  if (state->config.transition_enabled) {+    chroma_transition_init_output(output, state->config.transition_duration_ms);+  }++  if (!output->surface) {+    int ret = chroma_surface_create(state, output);+    if (ret != CHROMA_OK) {+      chroma_log("ERROR", "Failed to create surface for output %s",+                 output_name);+      if (expanded)+        free(expanded);+      return ret;+    }+  }++  int ret = chroma_render_wallpaper(state, output);+  if (ret != CHROMA_OK) {+    chroma_log("ERROR", "Failed to render wallpaper for output %s",+               output_name);+    if (expanded)+      free(expanded);+    return ret;+  }++  if (expanded)+    free(expanded);++  chroma_log("INFO", "Set wallpaper for output %s: %s", output_name, path);+  return CHROMA_OK;+}++int chroma_set_wallpaper_for_all(chroma_state_t *state,+                                 const char *image_path) {+  if (!state || !image_path) {+    return CHROMA_ERROR_INIT;+  }++  char *expanded = chroma_expand_path(image_path);+  const char *path = expanded ? expanded : image_path;++  if (!chroma_path_exists(path)) {+    chroma_log("ERROR", "Image not found: %s", path);+    if (expanded)+      free(expanded);+    return CHROMA_ERROR_IMAGE;+  }++  int success = 0;+  for (int i = 0; i < state->output_count; i++) {+    chroma_output_t *output = &state->outputs[i];+    if (!output->active)+      continue;++    if (chroma_set_wallpaper_for_output(state,+                                        output->name ? output->name : "unknown",+                                        path) == CHROMA_OK) {+      success++;+    }+  }++  if (expanded)+    free(expanded);++  return (success > 0) ? CHROMA_OK : CHROMA_ERROR_IMAGE;+}++int chroma_set_solid_color_for_output(chroma_state_t *state,+                                      const char *output_name,+                                      const char *hex_color) {+  if (!state || !output_name) {+    return CHROMA_ERROR_INIT;+  }++  bool valid = false;+  uint32_t color = parse_hex_color(hex_color, &valid);++  chroma_output_t *output = chroma_output_find_by_name(state, output_name);+  if (!output) {+    chroma_log("ERROR", "Output not found: %s", output_name);+    return CHROMA_ERROR_INIT;+  }++  chroma_image_t *img = NULL;+  char color_path[MAX_PATH_LEN];+  snprintf(color_path, sizeof(color_path), "color:#%08X", color);+  img = chroma_image_find_by_path(state, color_path);+  if (img) {+    img->ref_count++;+    chroma_log("DEBUG", "Reusing solid color image: %s", color_path);+  } else {+    for (int i = 0; i < state->image_count; i++) {+      if (!state->images[i].loaded) {+        img = &state->images[i];+        break;+      }+    }+    if (!img) {+      if (state->image_count >= MAX_OUTPUTS) {+        chroma_log("ERROR", "Maximum number of images reached (%d)",+                   MAX_OUTPUTS);+        return CHROMA_ERROR_MEMORY;+      }+      img = &state->images[state->image_count];+      state->image_count++;+    }+    int load_ret =+        chroma_image_load_color(img, color, output->width, output->height);+    if (load_ret != CHROMA_OK) {+      return load_ret;+    }+  }++  int ret;++  if (output->image) {+    chroma_image_release(output->image);+  }+  output->image = img;++  if (state->config.transition_enabled) {+    chroma_transition_init_output(output, state->config.transition_duration_ms);+  }++  if (!output->surface) {+    ret = chroma_surface_create(state, output);+    if (ret != CHROMA_OK)+      return ret;+  }++  ret = chroma_render_wallpaper(state, output);+  if (ret != CHROMA_OK)+    return ret;++  chroma_log("INFO", "Set solid color for output %s: %s", output_name,+             hex_color ? hex_color : "none");+  return CHROMA_OK;+}++int chroma_set_solid_color_for_all(chroma_state_t *state,+                                   const char *hex_color) {+  if (!state) {+    return CHROMA_ERROR_INIT;+  }++  int success = 0;+  for (int i = 0; i < state->output_count; i++) {+    chroma_output_t *output = &state->outputs[i];+    if (!output->active)+      continue;++    if (chroma_set_solid_color_for_output(+            state, output->name ? output->name : "unknown", hex_color) ==+        CHROMA_OK) {+      success++;+    }+  }++  return (success > 0) ? CHROMA_OK : CHROMA_ERROR_IMAGE;+}diff --git a/src/main.c b/src/main.cindex f2edbf8..9cc15b0 100644--- a/src/main.c+++ b/src/main.c@@ -134,6 +134,10 @@ int main(int argc, char *argv[]) {     config_file = get_default_config_path();   } +  // Store config path in state for IPC reload+  strncpy(state.config_file, config_file, MAX_PATH_LEN - 1);+  state.config_file[MAX_PATH_LEN - 1] = '\0';+   // Daemonize if requested   if (daemon_mode) {     chroma_log("INFO", "Starting daemon mode");@@ -178,6 +182,11 @@ int main(int argc, char *argv[]) {   }   chroma_log_memory_stats("post-wayland-connect"); +  // Initialize IPC socket for runtime commands+  if (chroma_ipc_init(&state) != CHROMA_OK) {+    chroma_log("WARN", "Failed to initialize IPC socket");+  }+   chroma_log("INFO", "Chroma daemon initialized successfully");   chroma_log_memory_stats("pre-main-loop");