notashelf /
292491b7388d7799a4e5ed0ddeb0265872b2f9d2
chroma
publicLightweight wallpaper daemon for Wayland
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.gitCommit 292491b7388d
tarballunverified2 files changed+554-0
@@ -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;+}@@ -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");