initial commit

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964ee9e6ebe436ca8328c6e4a7ec7c9d8d4
This commit is contained in:
raf 2025-09-29 15:12:03 +03:00
commit fcc080871a
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
11 changed files with 12536 additions and 0 deletions

222
include/chroma.h Normal file
View file

@ -0,0 +1,222 @@
#ifndef CHROMA_H
#define CHROMA_H
#include "wlr-layer-shell-unstable-v1.h"
#include "xdg-shell.h"
#include <EGL/egl.h>
#include <GL/gl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <sys/types.h>
#include <wayland-client.h>
#include <wayland-egl.h>
#define CHROMA_VERSION "1.0.0"
#define MAX_OUTPUTS 16
#define MAX_PATH_LEN 4096
#define CONFIG_FILE_NAME "chroma.conf"
// Error codes
typedef enum {
CHROMA_OK = 0,
CHROMA_ERROR_INIT = -1,
CHROMA_ERROR_WAYLAND = -2,
CHROMA_ERROR_EGL = -3,
CHROMA_ERROR_IMAGE = -4,
CHROMA_ERROR_CONFIG = -5,
CHROMA_ERROR_MEMORY = -6
} chroma_error_t;
// Image data structure
typedef struct {
unsigned char *data; // RGBA pixel data
int width;
int height;
int channels;
char path[MAX_PATH_LEN];
bool loaded;
} chroma_image_t;
// Wayland output information
typedef struct {
struct wl_output *wl_output;
uint32_t id;
int32_t x, y;
int32_t width, height;
int32_t scale;
enum wl_output_transform transform;
char *name;
char *description;
bool active;
// Back reference to state
struct chroma_state *state;
// Rendering context
struct wl_surface *surface;
struct zwlr_layer_surface_v1 *layer_surface;
struct wl_egl_window *egl_window;
EGLSurface egl_surface;
uint32_t configure_serial;
// Associated wallpaper
chroma_image_t *image;
} chroma_output_t;
// Config mapping structure
typedef struct {
char output_name[256];
char image_path[MAX_PATH_LEN];
} chroma_config_mapping_t;
// Application configuration
typedef struct {
chroma_config_mapping_t mappings[MAX_OUTPUTS];
int mapping_count;
char default_image[MAX_PATH_LEN];
bool daemon_mode;
} chroma_config_t;
// Main application state
typedef struct chroma_state {
// Wayland globals
struct wl_display *display;
struct wl_registry *registry;
struct wl_compositor *compositor;
struct zwlr_layer_shell_v1 *layer_shell;
// EGL context
EGLDisplay egl_display;
EGLContext egl_context;
EGLConfig egl_config;
// Outputs
chroma_output_t outputs[MAX_OUTPUTS];
int output_count;
// Images
chroma_image_t images[MAX_OUTPUTS];
int image_count;
// Configuration
chroma_config_t config;
// State flags
bool running;
bool initialized;
} chroma_state_t;
// Function declarations
// Initialization and cleanup
int chroma_init(chroma_state_t *state);
void chroma_cleanup(chroma_state_t *state);
// Wayland management
int chroma_wayland_connect(chroma_state_t *state);
void chroma_wayland_disconnect(chroma_state_t *state);
void chroma_registry_listener(void *data, struct wl_registry *registry,
uint32_t id, const char *interface,
uint32_t version);
void chroma_registry_remove(void *data, struct wl_registry *registry,
uint32_t id);
// Output management
int chroma_output_add(chroma_state_t *state, uint32_t id,
struct wl_output *output);
void chroma_output_remove(chroma_state_t *state, uint32_t id);
chroma_output_t *chroma_output_find_by_id(chroma_state_t *state, uint32_t id);
chroma_output_t *chroma_output_find_by_name(chroma_state_t *state,
const char *name);
// Output event handlers
void chroma_output_geometry(void *data, struct wl_output *output, int32_t x,
int32_t y, int32_t physical_width,
int32_t physical_height, int32_t subpixel,
const char *make, const char *model,
int32_t transform);
void chroma_output_mode(void *data, struct wl_output *output, uint32_t flags,
int32_t width, int32_t height, int32_t refresh);
void chroma_output_scale(void *data, struct wl_output *output, int32_t scale);
void chroma_output_name(void *data, struct wl_output *output, const char *name);
void chroma_output_description(void *data, struct wl_output *output,
const char *description);
void chroma_output_done(void *data, struct wl_output *output);
// EGL and rendering
int chroma_egl_init(chroma_state_t *state);
void chroma_egl_cleanup(chroma_state_t *state);
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output);
void chroma_surface_destroy(chroma_output_t *output);
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output);
// Layer shell functions
void chroma_layer_surface_configure(void *data,
struct zwlr_layer_surface_v1 *layer_surface,
uint32_t serial, uint32_t width,
uint32_t height);
void chroma_layer_surface_closed(void *data,
struct zwlr_layer_surface_v1 *layer_surface);
// Image loading
void chroma_image_init_stb(void);
int chroma_image_load(chroma_image_t *image, const char *path);
void chroma_image_free(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);
int chroma_image_validate(const char *path);
int chroma_image_get_info(const char *path, int *width, int *height,
int *channels);
void chroma_images_cleanup(chroma_state_t *state);
// Configuration
int chroma_config_load(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);
// Main loop and events
int chroma_run(chroma_state_t *state);
void chroma_handle_signals(void);
int chroma_reload_config(chroma_state_t *state, const char *config_file);
int chroma_update_outputs(chroma_state_t *state);
void chroma_get_stats(chroma_state_t *state, int *active_outputs,
int *loaded_images);
void handle_output_done(chroma_state_t *state, chroma_output_t *output);
// Utilities
void chroma_log(const char *level, const char *format, ...);
const char *chroma_error_string(chroma_error_t error);
void chroma_set_log_level(int level);
int chroma_get_log_level(void);
void chroma_set_signal_state(chroma_state_t *state, const char *config_file);
void chroma_cleanup_signals(void);
char *chroma_expand_path(const char *path);
int chroma_mkdir_recursive(const char *path, mode_t mode);
char *chroma_get_config_dir(void);
bool chroma_path_exists(const char *path);
bool chroma_is_regular_file(const char *path);
bool chroma_is_directory(const char *path);
long chroma_get_file_size(const char *path);
const char *chroma_get_file_extension(const char *path);
int chroma_strcasecmp(const char *s1, const char *s2);
size_t chroma_strlcpy(char *dst, const char *src, size_t size);
size_t chroma_strlcat(char *dst, const char *src, size_t size);
long long chroma_get_time_ms(void);
void chroma_sleep_ms(long ms);
void chroma_format_memory_size(size_t bytes, char *buffer, size_t buffer_size);
void chroma_utils_cleanup(void);
// Listener structures
extern const struct wl_registry_listener chroma_registry_listener_impl;
extern const struct wl_output_listener chroma_output_listener_impl;
extern const struct zwlr_layer_surface_v1_listener
chroma_layer_surface_listener_impl;
// Global state for signal handling
extern volatile sig_atomic_t chroma_should_quit;
#endif // CHROMA_H

7988
include/stb_image.h Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_layer_shell_unstable_v1">
<copyright>
Copyright © 2017 Drew DeVault
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<interface name="zwlr_layer_shell_v1" version="4">
<description summary="create surfaces that are layers of the desktop">
Clients can use this interface to assign the surface_layer role to
wl_surfaces. Such surfaces are assigned to a "layer" of the output and
rendered with a defined z-depth respective to each other. They may also be
anchored to the edges and corners of a screen and specify input handling
semantics. This interface should be suitable for the implementation of
many desktop shell components, and a broad number of other applications
that interact with the desktop.
</description>
<request name="get_layer_surface">
<description summary="create a layer_surface from a surface">
Create a layer surface for an existing surface. This assigns the role of
layer_surface, or raises a protocol error if another role is already
assigned.
Creating a layer surface from a wl_surface which has a buffer attached
or committed is a client error, and any attempts by a client to attach
or manipulate a buffer prior to the first layer_surface.configure call
must also be treated as errors.
After creating a layer_surface object and setting it up, the client
must perform an initial commit without any buffer attached.
The compositor will reply with a layer_surface.configure event.
The client must acknowledge it and is then allowed to attach a buffer
to map the surface.
You may pass NULL for output to allow the compositor to decide which
output to use. Generally this will be the one that the user most
recently interacted with.
Clients can specify a namespace that defines the purpose of the layer
surface.
</description>
<arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
<arg name="surface" type="object" interface="wl_surface"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
<arg name="namespace" type="string" summary="namespace for the layer surface"/>
</request>
<enum name="error">
<entry name="role" value="0" summary="wl_surface has another role"/>
<entry name="invalid_layer" value="1" summary="layer value is invalid"/>
<entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
</enum>
<enum name="layer">
<description summary="available layers for surfaces">
These values indicate which layers a surface can be rendered in. They
are ordered by z depth, bottom-most first. Traditional shell surfaces
will typically be rendered between the bottom and top layers.
Fullscreen shell surfaces are typically rendered at the top layer.
Multiple surfaces can share a single layer, and ordering within a
single layer is undefined.
</description>
<entry name="background" value="0"/>
<entry name="bottom" value="1"/>
<entry name="top" value="2"/>
<entry name="overlay" value="3"/>
</enum>
<!-- Version 3 additions -->
<request name="destroy" type="destructor" since="3">
<description summary="destroy the layer_shell object">
This request indicates that the client will not use the layer_shell
object any more. Objects that have been created through this instance
are not affected.
</description>
</request>
</interface>
<interface name="zwlr_layer_surface_v1" version="4">
<description summary="layer metadata interface">
An interface that may be implemented by a wl_surface, for surfaces that
are designed to be rendered as a layer of a stacked desktop-like
environment.
Layer surface state (layer, size, anchor, exclusive zone,
margin, interactivity) is double-buffered, and will be applied at the
time wl_surface.commit of the corresponding wl_surface is called.
Attaching a null buffer to a layer surface unmaps it.
Unmapping a layer_surface means that the surface cannot be shown by the
compositor until it is explicitly mapped again. The layer_surface
returns to the state it had right after layer_shell.get_layer_surface.
The client can re-map the surface by performing a commit without any
buffer attached, waiting for a configure event and handling it as usual.
</description>
<request name="set_size">
<description summary="sets the size of the surface">
Sets the size of the surface in surface-local coordinates. The
compositor will display the surface centered with respect to its
anchors.
If you pass 0 for either value, the compositor will assign it and
inform you of the assignment in the configure event. You must set your
anchor to opposite edges in the dimensions you omit; not doing so is a
protocol error. Both values are 0 by default.
Size is double-buffered, see wl_surface.commit.
</description>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</request>
<request name="set_anchor">
<description summary="configures the anchor point of the surface">
Requests that the compositor anchor the surface to the specified edges
and corners. If two orthogonal edges are specified (e.g. 'top' and
'left'), then the anchor point will be the intersection of the edges
(e.g. the top left corner of the output); otherwise the anchor point
will be centered on that edge, or in the center if none is specified.
Anchor is double-buffered, see wl_surface.commit.
</description>
<arg name="anchor" type="uint" enum="anchor"/>
</request>
<request name="set_exclusive_zone">
<description summary="configures the exclusive geometry of this surface">
Requests that the compositor avoids occluding an area with other
surfaces. The compositor's use of this information is
implementation-dependent - do not assume that this region will not
actually be occluded.
A positive value is only meaningful if the surface is anchored to one
edge or an edge and both perpendicular edges. If the surface is not
anchored, anchored to only two perpendicular edges (a corner), anchored
to only two parallel edges or anchored to all edges, a positive value
will be treated the same as zero.
A positive zone is the distance from the edge in surface-local
coordinates to consider exclusive.
Surfaces that do not wish to have an exclusive zone may instead specify
how they should interact with surfaces that do. If set to zero, the
surface indicates that it would like to be moved to avoid occluding
surfaces with a positive exclusive zone. If set to -1, the surface
indicates that it would not like to be moved to accommodate for other
surfaces, and the compositor should extend it all the way to the edges
it is anchored to.
For example, a panel might set its exclusive zone to 10, so that
maximized shell surfaces are not shown on top of it. A notification
might set its exclusive zone to 0, so that it is moved to avoid
occluding the panel, but shell surfaces are shown underneath it. A
wallpaper or lock screen might set their exclusive zone to -1, so that
they stretch below or over the panel.
The default value is 0.
Exclusive zone is double-buffered, see wl_surface.commit.
</description>
<arg name="zone" type="int"/>
</request>
<request name="set_margin">
<description summary="sets a margin from the anchor point">
Requests that the surface be placed some distance away from the anchor
point on the output, in surface-local coordinates. Setting this value
for edges you are not anchored to has no effect.
The exclusive zone includes the margin.
Margin is double-buffered, see wl_surface.commit.
</description>
<arg name="top" type="int"/>
<arg name="right" type="int"/>
<arg name="bottom" type="int"/>
<arg name="left" type="int"/>
</request>
<enum name="keyboard_interactivity">
<description summary="types of keyboard interaction possible for a layer shell surface">
Types of keyboard interaction possible for layer shell surfaces. The
rationale for this is twofold: (1) some applications are not interested
in keyboard events and not allowing them to be focused can improve the
desktop experience; (2) some applications will want to take exclusive
keyboard focus.
</description>
<entry name="none" value="0">
<description summary="no keyboard focus is possible">
This value indicates that this surface is not interested in keyboard
events and the compositor should never assign it the keyboard focus.
This is the default value, set for newly created layer shell surfaces.
This is useful for e.g. desktop widgets that display information or
only have interaction with non-keyboard input devices.
</description>
</entry>
<entry name="exclusive" value="1">
<description summary="request exclusive keyboard focus">
Request exclusive keyboard focus if this surface is above the shell surface layer.
For the top and overlay layers, the seat will always give
exclusive keyboard focus to the top-most layer which has keyboard
interactivity set to exclusive. If this layer contains multiple
surfaces with keyboard interactivity set to exclusive, the compositor
determines the one receiving keyboard events in an implementation-
defined manner. In this case, no guarantee is made when this surface
will receive keyboard focus (if ever).
For the bottom and background layers, the compositor is allowed to use
normal focus semantics.
This setting is mainly intended for applications that need to ensure
they receive all keyboard events, such as a lock screen or a password
prompt.
</description>
</entry>
<entry name="on_demand" value="2" since="4">
<description summary="request regular keyboard focus semantics">
This requests the compositor to allow this surface to be focused and
unfocused by the user in an implementation-defined manner. The user
should be able to unfocus this surface even regardless of the layer
it is on.
Typically, the compositor will want to use its normal mechanism to
manage keyboard focus between layer shell surfaces with this setting
and regular toplevels on the desktop layer (e.g. click to focus).
Nevertheless, it is possible for a compositor to require a special
interaction to focus or unfocus layer shell surfaces (e.g. requiring
a click even if focus follows the mouse normally, or providing a
keybinding to switch focus between layers).
This setting is mainly intended for desktop shell components (e.g.
panels) that allow keyboard interaction. Using this option can allow
implementing a desktop shell that can be fully usable without the
mouse.
</description>
</entry>
</enum>
<request name="set_keyboard_interactivity">
<description summary="requests keyboard events">
Set how keyboard events are delivered to this surface. By default,
layer shell surfaces do not receive keyboard events; this request can
be used to change this.
This setting is inherited by child surfaces set by the get_popup
request.
Layer surfaces receive pointer, touch, and tablet events normally. If
you do not want to receive them, set the input region on your surface
to an empty region.
Keyboard interactivity is double-buffered, see wl_surface.commit.
</description>
<arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
</request>
<request name="get_popup">
<description summary="assign this layer_surface as an xdg_popup parent">
This assigns an xdg_popup's parent to this layer_surface. This popup
should have been created via xdg_surface::get_popup with the parent set
to NULL, and this request must be invoked before committing the popup's
initial state.
See the documentation of xdg_popup for more details about what an
xdg_popup is and how it is used.
</description>
<arg name="popup" type="object" interface="xdg_popup"/>
</request>
<request name="ack_configure">
<description summary="ack a configure event">
When a configure event is received, if a client commits the
surface in response to the configure event, then the client
must make an ack_configure request sometime before the commit
request, passing along the serial of the configure event.
If the client receives multiple configure events before it
can respond to one, it only has to ack the last configure event.
A client is not required to commit immediately after sending
an ack_configure request - it may even ack_configure several times
before its next surface commit.
A client may send multiple ack_configure requests before committing, but
only the last request sent before a commit indicates which configure
event the client really is responding to.
</description>
<arg name="serial" type="uint" summary="the serial from the configure event"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the layer_surface">
This request destroys the layer surface.
</description>
</request>
<event name="configure">
<description summary="suggest a surface change">
The configure event asks the client to resize its surface.
Clients should arrange their surface for the new states, and then send
an ack_configure request with the serial sent in this configure event at
some point before committing the new surface.
The client is free to dismiss all but the last configure event it
received.
The width and height arguments specify the size of the window in
surface-local coordinates.
The size is a hint, in the sense that the client is free to ignore it if
it doesn't resize, pick a smaller size (to satisfy aspect ratio or
resize in steps of NxM pixels). If the client picks a smaller size and
is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
surface will be centered on this axis.
If the width or height arguments are zero, it means the client should
decide its own window dimension.
</description>
<arg name="serial" type="uint"/>
<arg name="width" type="uint"/>
<arg name="height" type="uint"/>
</event>
<event name="closed">
<description summary="surface should be closed">
The closed event is sent by the compositor when the surface will no
longer be shown. The output may have been destroyed or the user may
have asked for it to be removed. Further changes to the surface will be
ignored. The client should destroy the resource after receiving this
event, and create a new surface if they so choose.
</description>
</event>
<enum name="error">
<entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
<entry name="invalid_size" value="1" summary="size is invalid"/>
<entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
<entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
</enum>
<enum name="anchor" bitfield="true">
<entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
<entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
<entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
<entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
</enum>
<!-- Version 2 additions -->
<request name="set_layer" since="2">
<description summary="change the layer of the surface">
Change the layer that the surface is rendered on.
Layer is double-buffered, see wl_surface.commit.
</description>
<arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
</request>
</interface>
</protocol>

1415
protocols/xdg-shell.xml Normal file

File diff suppressed because it is too large Load diff

326
src/config.c Normal file
View file

@ -0,0 +1,326 @@
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../include/chroma.h"
// Default configuration values
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
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 integer value from string
// Output-to-image mapping
static int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path) {
if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT;
}
if (config->mapping_count >= MAX_OUTPUTS) {
chroma_log("ERROR", "Maximum number of output mappings reached (%d)",
MAX_OUTPUTS);
return CHROMA_ERROR_MEMORY;
}
chroma_config_mapping_t *mapping = &config->mappings[config->mapping_count];
strncpy(mapping->output_name, output_name, sizeof(mapping->output_name) - 1);
mapping->output_name[sizeof(mapping->output_name) - 1] = '\0';
strncpy(mapping->image_path, image_path, sizeof(mapping->image_path) - 1);
mapping->image_path[sizeof(mapping->image_path) - 1] = '\0';
config->mapping_count++;
chroma_log("DEBUG", "Added mapping: %s -> %s", output_name, image_path);
return CHROMA_OK;
}
// Initialize configuration with defaults
static void init_default_config(chroma_config_t *config) {
if (!config)
return;
memset(config, 0, sizeof(chroma_config_t));
config->daemon_mode = false;
config->mapping_count = 0;
// 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");
}
}
// 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) {
strncpy(config->default_image, value, sizeof(config->default_image) - 1);
config->default_image[sizeof(config->default_image) - 1] = '\0';
chroma_log("DEBUG", "Set default image: %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");
} 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;
}
// Validate image path
if (chroma_image_validate(value) != CHROMA_OK) {
chroma_log("WARN", "Invalid image path for output %s: %s", output_name,
value);
return CHROMA_OK;
}
if (add_output_mapping(config, output_name, value) != CHROMA_OK) {
chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
value);
return CHROMA_ERROR_CONFIG;
}
} else {
chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
key);
}
return CHROMA_OK;
}
// Load configuration from file
int chroma_config_load(chroma_config_t *config, const char *config_file) {
if (!config) {
return CHROMA_ERROR_INIT;
}
// Initialize with defaults
init_default_config(config);
if (!config_file) {
chroma_log("INFO", "No config file specified, using defaults");
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;
}
}
chroma_log("INFO", "Loading configuration from: %s", 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++;
}
}
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);
return CHROMA_OK;
}
// Free configuration resources
void chroma_config_free(chroma_config_t *config) {
if (!config) {
return;
}
memset(config->mappings, 0, sizeof(config->mappings));
config->mapping_count = 0;
memset(config->default_image, 0, sizeof(config->default_image));
chroma_log("DEBUG", "Freed configuration");
}
// Get image path for specific output
const char *chroma_config_get_image_for_output(chroma_config_t *config,
const char *output_name) {
if (!config || !output_name) {
return NULL;
}
// Look for specific output mapping
for (int i = 0; i < config->mapping_count; i++) {
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
chroma_log("DEBUG", "Found specific mapping for output %s: %s",
output_name, config->mappings[i].image_path);
return config->mappings[i].image_path;
}
}
// Return default image if no specific mapping found
if (strlen(config->default_image) > 0) {
chroma_log("DEBUG", "Using default image for output %s: %s", output_name,
config->default_image);
return config->default_image;
}
chroma_log("WARN", "No image configured for output: %s", output_name);
return NULL;
}
// Create a sample configuration file
int chroma_config_create_sample(const char *config_file) {
if (!config_file) {
return CHROMA_ERROR_INIT;
}
FILE *file = fopen(config_file, "w");
if (!file) {
chroma_log("ERROR", "Failed to create sample config file %s: %s",
config_file, strerror(errno));
return CHROMA_ERROR_CONFIG;
}
fprintf(file, "# Chroma Wallpaper Daemon Configuration\n");
fprintf(file, "# Lines starting with # are comments\n\n");
fprintf(file, "# Default wallpaper for outputs without specific mapping\n");
fprintf(file, "default_image = ~/.config/chroma/default.jpg\n\n");
fprintf(file, "# Output-specific wallpapers\n");
fprintf(file, "# Format: output.OUTPUT_NAME = /path/to/image.jpg\n");
fprintf(file, "# You can find output names using: wlr-randr\n");
fprintf(file, "\n");
fprintf(file, "# Examples:\n");
fprintf(file, "# output.DP-1 = ~/.config/chroma/monitor1.jpg\n");
fprintf(file, "# output.DP-2 = ~/.config/chroma/monitor2.png\n");
fprintf(file, "# output.HDMI-A-1 = ~/.config/chroma/hdmi.jpg\n");
fclose(file);
chroma_log("INFO", "Created sample configuration file: %s", config_file);
return CHROMA_OK;
}
// Print current configuration for debugging
void chroma_config_print(const chroma_config_t *config) {
if (!config) {
return;
}
chroma_log("INFO", "=== Configuration ===");
chroma_log("INFO", "Default image: %s", config->default_image);
chroma_log("INFO", "Daemon mode: %s", config->daemon_mode ? "true" : "false");
chroma_log("INFO", "Output mappings: %d", config->mapping_count);
for (int i = 0; i < config->mapping_count; i++) {
chroma_log("INFO", " %s -> %s", config->mappings[i].output_name,
config->mappings[i].image_path);
}
chroma_log("INFO", "====================");
}

439
src/core.c Normal file
View file

@ -0,0 +1,439 @@
#include <errno.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "../include/chroma.h"
// Global logging level
static int log_level = 0; // 0=ERROR, 1=WARN, 2=INFO, 3=DEBUG
// Initialize chroma state
int chroma_init(chroma_state_t *state) {
if (!state) {
return CHROMA_ERROR_INIT;
}
// Initialize all fields to zero
memset(state, 0, sizeof(chroma_state_t));
// Set initial state
state->running = false;
state->initialized = false;
state->egl_display = EGL_NO_DISPLAY;
state->egl_context = EGL_NO_CONTEXT;
// Initialize stb_image
chroma_image_init_stb();
state->initialized = true;
chroma_log("INFO", "Chroma state initialized");
return CHROMA_OK;
}
// Cleanup chroma state
void chroma_cleanup(chroma_state_t *state) {
if (!state || !state->initialized) {
return;
}
chroma_log("INFO", "Cleaning up chroma state");
// Stop the main loop
state->running = false;
// Clean up all images
chroma_images_cleanup(state);
// Clean up EGL
chroma_egl_cleanup(state);
// Clean up Wayland
chroma_wayland_disconnect(state);
// Clean up configuration
chroma_config_free(&state->config);
state->initialized = false;
chroma_log("INFO", "Chroma cleanup complete");
}
// Assign wallpaper to an output
static int assign_wallpaper_to_output(chroma_state_t *state,
chroma_output_t *output) {
if (!state || !output || !output->active) {
return CHROMA_ERROR_INIT;
}
// Get image path for this output
const char *image_path = chroma_config_get_image_for_output(
&state->config, output->name ? output->name : "unknown");
if (!image_path) {
chroma_log("WARN", "No wallpaper configured for output %u (%s)", output->id,
output->name ? output->name : "unknown");
return CHROMA_ERROR_CONFIG;
}
// Load or get cached image
chroma_image_t *image = chroma_image_get_or_load(state, image_path);
if (!image) {
chroma_log("ERROR", "Failed to load image for output %u: %s", output->id,
image_path);
return CHROMA_ERROR_IMAGE;
}
// Assign image to output
output->image = image;
// Create surface if it doesn't exist
if (!output->surface) {
int ret = chroma_surface_create(state, output);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to create surface for output %u", output->id);
return ret;
}
}
// 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);
return ret;
}
chroma_log("INFO", "Assigned wallpaper to output %u (%s): %s", output->id,
output->name ? output->name : "unknown", image_path);
return CHROMA_OK;
}
// Assign wallpapers to all active outputs
static int assign_wallpapers_to_all_outputs(chroma_state_t *state) {
if (!state) {
return CHROMA_ERROR_INIT;
}
int success_count = 0;
int error_count = 0;
for (int i = 0; i < state->output_count; i++) {
chroma_output_t *output = &state->outputs[i];
if (!output->active) {
chroma_log("DEBUG", "Skipping inactive output %u", output->id);
continue;
}
if (assign_wallpaper_to_output(state, output) == CHROMA_OK) {
success_count++;
} else {
error_count++;
}
}
chroma_log("INFO", "Wallpaper assignment complete: %d success, %d errors",
success_count, error_count);
return (success_count > 0) ? CHROMA_OK : CHROMA_ERROR_IMAGE;
}
// Handle output configuration complete event
void handle_output_done(chroma_state_t *state, chroma_output_t *output) {
if (!state || !output) {
return;
}
chroma_log("INFO",
"Output %u (%s) configuration complete: %dx%d@%d, scale=%d",
output->id, output->name ? output->name : "unknown", output->width,
output->height, 0, output->scale);
/* Assign wallpaper to this output */
if (assign_wallpaper_to_output(state, output) != CHROMA_OK) {
chroma_log("ERROR", "Failed to assign wallpaper to output %u", output->id);
}
}
// Process Wayland events
static int process_wayland_events(chroma_state_t *state) {
if (!state || !state->display) {
return CHROMA_ERROR_WAYLAND;
}
/* Dispatch pending events */
if (wl_display_dispatch_pending(state->display) == -1) {
chroma_log("ERROR", "Failed to dispatch pending Wayland events: %s",
strerror(errno));
return CHROMA_ERROR_WAYLAND;
}
/* Read events from the server */
if (wl_display_read_events(state->display) == -1) {
chroma_log("ERROR", "Failed to read Wayland events: %s", strerror(errno));
return CHROMA_ERROR_WAYLAND;
}
/* Dispatch the read events */
if (wl_display_dispatch_pending(state->display) == -1) {
chroma_log("ERROR", "Failed to dispatch read Wayland events: %s",
strerror(errno));
return CHROMA_ERROR_WAYLAND;
}
return CHROMA_OK;
}
// Main event loop
int chroma_run(chroma_state_t *state) {
if (!state || !state->initialized) {
return CHROMA_ERROR_INIT;
}
chroma_log("INFO", "Starting main event loop");
state->running = true;
// Initial wallpaper assignment
chroma_log("INFO", "Performing initial wallpaper assignment");
assign_wallpapers_to_all_outputs(state);
// Main event loop
while (state->running && !chroma_should_quit) {
// Dispatch any pending events first
if (wl_display_dispatch_pending(state->display) == -1) {
chroma_log("ERROR", "Failed to dispatch pending events: %s",
strerror(errno));
break;
}
// Prepare to read events
if (wl_display_prepare_read(state->display) == -1) {
chroma_log("ERROR", "Failed to prepare Wayland display for reading");
break;
}
// Flush outgoing requests
if (wl_display_flush(state->display) == -1) {
chroma_log("ERROR", "Failed to flush Wayland display: %s",
strerror(errno));
wl_display_cancel_read(state->display);
break;
}
// Get the display file descriptor
int fd = wl_display_get_fd(state->display);
if (fd == -1) {
chroma_log("ERROR", "Failed to get Wayland display file descriptor");
wl_display_cancel_read(state->display);
break;
}
// Use select() to wait for events with timeout
fd_set readfds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 1; // 1 second timeout
timeout.tv_usec = 0;
int select_result = select(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;
}
chroma_log("ERROR", "select() failed: %s", strerror(errno));
wl_display_cancel_read(state->display);
break;
}
if (select_result == 0) {
// Timeout - no events available
wl_display_cancel_read(state->display);
continue;
}
// Events are available
if (FD_ISSET(fd, &readfds)) {
if (process_wayland_events(state) != CHROMA_OK) {
break;
}
} else {
wl_display_cancel_read(state->display);
}
}
state->running = false;
chroma_log("INFO", "Main event loop ended");
return CHROMA_OK;
}
// Error code to string conversion
const char *chroma_error_string(chroma_error_t error) {
switch (error) {
case CHROMA_OK:
return "Success";
case CHROMA_ERROR_INIT:
return "Initialization error";
case CHROMA_ERROR_WAYLAND:
return "Wayland error";
case CHROMA_ERROR_EGL:
return "EGL error";
case CHROMA_ERROR_IMAGE:
return "Image loading error";
case CHROMA_ERROR_CONFIG:
return "Configuration error";
case CHROMA_ERROR_MEMORY:
return "Memory allocation error";
default:
return "Unknown error";
}
}
// Logging function
void chroma_log(const char *level, const char *format, ...) {
va_list args;
char timestamp[32];
struct timeval tv;
struct tm *tm_info;
// Get current time
gettimeofday(&tv, NULL);
tm_info = localtime(&tv.tv_sec);
// Format timestamp
snprintf(timestamp, sizeof(timestamp), "%04d-%02d-%02d %02d:%02d:%02d.%03d",
tm_info->tm_year + 1900, tm_info->tm_mon + 1, tm_info->tm_mday,
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec,
(int)(tv.tv_usec / 1000));
// Print log message
printf("[%s] %s: ", timestamp, level);
va_start(args, format);
vprintf(format, args);
va_end(args);
printf("\n");
fflush(stdout);
}
// Set log level
void chroma_set_log_level(int level) { log_level = level; }
// Get log level
int chroma_get_log_level(void) { return log_level; }
// Handle configuration reload (SIGHUP)
int chroma_reload_config(chroma_state_t *state, const char *config_file) {
if (!state) {
return CHROMA_ERROR_INIT;
}
chroma_log("INFO", "Reloading configuration");
// Free current configuration
chroma_config_free(&state->config);
// Load new configuration
int ret = chroma_config_load(&state->config, config_file);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to reload configuration: %s",
chroma_error_string(ret));
return ret;
}
// Reassign wallpapers with new configuration
ret = assign_wallpapers_to_all_outputs(state);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to reassign wallpapers after config reload");
return ret;
}
chroma_log("INFO", "Configuration reloaded successfully");
return CHROMA_OK;
}
// Check if an output needs wallpaper update
static bool output_needs_update(chroma_output_t *output) {
if (!output || !output->active) {
return false;
}
// Check if output has no surface or image assigned
if (!output->surface || !output->image) {
return true;
}
// Check if image is no longer loaded
if (!output->image->loaded) {
return true;
}
return false;
}
// Update outputs that need wallpaper refresh
int chroma_update_outputs(chroma_state_t *state) {
if (!state) {
return CHROMA_ERROR_INIT;
}
int updated_count = 0;
for (int i = 0; i < state->output_count; i++) {
chroma_output_t *output = &state->outputs[i];
if (output_needs_update(output)) {
if (assign_wallpaper_to_output(state, output) == CHROMA_OK) {
updated_count++;
}
}
}
if (updated_count > 0) {
chroma_log("INFO", "Updated wallpapers for %d outputs", updated_count);
}
return CHROMA_OK;
}
// Get statistics
void chroma_get_stats(chroma_state_t *state, int *active_outputs,
int *loaded_images) {
if (!state) {
if (active_outputs)
*active_outputs = 0;
if (loaded_images)
*loaded_images = 0;
return;
}
int active = 0;
for (int i = 0; i < state->output_count; i++) {
if (state->outputs[i].active) {
active++;
}
}
int loaded = 0;
for (int i = 0; i < state->image_count; i++) {
if (state->images[i].loaded) {
loaded++;
}
}
if (active_outputs)
*active_outputs = active;
if (loaded_images)
*loaded_images = loaded;
}

268
src/image.c Normal file
View file

@ -0,0 +1,268 @@
#define STB_IMAGE_IMPLEMENTATION
#include "../include/stb_image.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "../include/chroma.h"
// Check if file exists and is readable
static int file_exists(const char *path) {
struct stat st;
return (stat(path, &st) == 0 && S_ISREG(st.st_mode));
}
// Get file size
static long get_file_size(const char *path) {
struct stat st;
if (stat(path, &st) != 0) {
return -1;
}
return st.st_size;
}
// Load image from file
int chroma_image_load(chroma_image_t *image, const char *path) {
if (!image || !path) {
chroma_log("ERROR", "Invalid parameters for image loading");
return CHROMA_ERROR_INIT;
}
// Initialize image structure
memset(image, 0, sizeof(chroma_image_t));
strncpy(image->path, path, MAX_PATH_LEN - 1);
image->path[MAX_PATH_LEN - 1] = '\0';
// Check if file exists
if (!file_exists(path)) {
chroma_log("ERROR", "Image file does not exist: %s", path);
return CHROMA_ERROR_IMAGE;
}
// Get file size for logging
long file_size = get_file_size(path);
if (file_size > 0) {
chroma_log("DEBUG", "Loading image: %s (%.2f MB)", path,
(double)file_size / (1024.0 * 1024.0));
}
// Load image data using stb_image
stbi_set_flip_vertically_on_load(0); // Keep images right-side up
image->data =
stbi_load(path, &image->width, &image->height, &image->channels, 0);
if (!image->data) {
chroma_log("ERROR", "Failed to load image %s: %s", path,
stbi_failure_reason());
return CHROMA_ERROR_IMAGE;
}
// Validate image dimensions
if (image->width <= 0 || image->height <= 0) {
chroma_log("ERROR", "Invalid image dimensions: %dx%d", image->width,
image->height);
chroma_image_free(image);
return CHROMA_ERROR_IMAGE;
}
// Check supported formats
if (image->channels < 3 || image->channels > 4) {
chroma_log("ERROR",
"Unsupported image format: %d channels (need RGB or RGBA)",
image->channels);
chroma_image_free(image);
return CHROMA_ERROR_IMAGE;
}
// Convert RGB to RGBA if necessary for consistent handling
if (image->channels == 3) {
int pixel_count = image->width * image->height;
unsigned char *rgba_data = malloc(pixel_count * 4);
if (!rgba_data) {
chroma_log("ERROR", "Failed to allocate memory for RGBA conversion");
chroma_image_free(image);
return CHROMA_ERROR_MEMORY;
}
// Convert RGB to RGBA
for (int i = 0; i < pixel_count; i++) {
rgba_data[i * 4 + 0] = image->data[i * 3 + 0]; // R
rgba_data[i * 4 + 1] = image->data[i * 3 + 1]; // G
rgba_data[i * 4 + 2] = image->data[i * 3 + 2]; // B
rgba_data[i * 4 + 3] = 255; // A
}
// Replace original data
stbi_image_free(image->data);
image->data = rgba_data;
image->channels = 4;
}
image->loaded = true;
chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)", path,
image->width, image->height, image->channels,
(double)(image->width * image->height * image->channels) /
(1024.0 * 1024.0));
return CHROMA_OK;
}
// Free image data
void chroma_image_free(chroma_image_t *image) {
if (!image) {
return;
}
if (image->data) {
if (image->channels == 4 && strlen(image->path) > 0) {
// If we converted from RGB to RGBA, use regular free()
free(image->data);
} else {
// If loaded directly by stb_image, use stbi_image_free()
stbi_image_free(image->data);
}
image->data = NULL;
}
image->width = 0;
image->height = 0;
image->channels = 0;
image->loaded = false;
if (strlen(image->path) > 0) {
chroma_log("DEBUG", "Freed image: %s", image->path);
}
memset(image->path, 0, sizeof(image->path));
}
// Find image by path in state
chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
const char *path) {
if (!state || !path) {
return NULL;
}
for (int i = 0; i < state->image_count; i++) {
if (strcmp(state->images[i].path, path) == 0) {
return &state->images[i];
}
}
return NULL;
}
// Load image if not already loaded
chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
const char *path) {
if (!state || !path) {
return NULL;
}
// 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);
return existing;
}
// Find empty slot or reuse existing
chroma_image_t *image = existing;
if (!image) {
if (state->image_count >= MAX_OUTPUTS) {
chroma_log("ERROR", "Maximum number of images reached");
return NULL;
}
image = &state->images[state->image_count];
state->image_count++;
}
// Load the image
if (chroma_image_load(image, path) != CHROMA_OK) {
// If this was a new slot, decrement count
if (!existing) {
state->image_count--;
}
return NULL;
}
return image;
}
// Validate image file format
int chroma_image_validate(const char *path) {
if (!path || !file_exists(path)) {
return CHROMA_ERROR_IMAGE;
}
// Check file extension (basic validation)
const char *ext = strrchr(path, '.');
if (!ext) {
return CHROMA_ERROR_IMAGE;
}
ext++; // Skip the dot
// Check supported extensions
if (strcasecmp(ext, "jpg") == 0 || strcasecmp(ext, "jpeg") == 0 ||
strcasecmp(ext, "png") == 0 || strcasecmp(ext, "bmp") == 0 ||
strcasecmp(ext, "tga") == 0 || strcasecmp(ext, "psd") == 0 ||
strcasecmp(ext, "gif") == 0 || strcasecmp(ext, "hdr") == 0 ||
strcasecmp(ext, "pic") == 0 || strcasecmp(ext, "ppm") == 0 ||
strcasecmp(ext, "pgm") == 0) {
return CHROMA_OK;
}
chroma_log("WARN", "Potentially unsupported image format: %s", ext);
return CHROMA_ERROR_IMAGE;
}
// Get image info without loading full data
int chroma_image_get_info(const char *path, int *width, int *height,
int *channels) {
if (!path || !width || !height || !channels) {
return CHROMA_ERROR_INIT;
}
if (!file_exists(path)) {
return CHROMA_ERROR_IMAGE;
}
if (!stbi_info(path, width, height, channels)) {
chroma_log("ERROR", "Failed to get image info for %s: %s", path,
stbi_failure_reason());
return CHROMA_ERROR_IMAGE;
}
return CHROMA_OK;
}
// Cleanup all images in state
void chroma_images_cleanup(chroma_state_t *state) {
if (!state) {
return;
}
for (int i = 0; i < state->image_count; i++) {
chroma_image_free(&state->images[i]);
}
state->image_count = 0;
chroma_log("INFO", "Cleaned up all images");
}
// Preload common image formats for validation
void chroma_image_init_stb(void) {
// Set stb_image options
stbi_set_flip_vertically_on_load(0);
// These could be made configurable
stbi_ldr_to_hdr_gamma(2.2f);
stbi_ldr_to_hdr_scale(1.0f);
chroma_log("DEBUG", "Initialized stb_image library");
}

238
src/main.c Normal file
View file

@ -0,0 +1,238 @@
#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "../include/chroma.h"
/* Global state for signal handling */
volatile sig_atomic_t chroma_should_quit = 0;
static void print_usage(const char *program_name) {
printf("Usage: %s [OPTIONS]\n", program_name);
printf("Minimal Wayland Multi-Monitor Wallpaper Daemon\n\n");
printf("Options:\n");
printf(" -c, --config FILE Configuration file path\n");
printf(" -d, --daemon Run as daemon\n");
printf(" -v, --verbose Verbose logging\n");
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 --daemon\n", program_name);
}
static void print_version(void) {
printf("chroma %s\n", CHROMA_VERSION);
printf("Minimal Wayland Multi-Monitor Wallpaper Daemon\n");
}
static void signal_handler(int sig) {
switch (sig) {
case SIGTERM:
case SIGINT:
chroma_should_quit = 1;
break;
case SIGHUP:
// TODO: Implement config reload
break;
}
}
static int setup_signals(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGTERM, &sa, NULL) == -1) {
perror("sigaction(SIGTERM)");
return -1;
}
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction(SIGINT)");
return -1;
}
if (sigaction(SIGHUP, &sa, NULL) == -1) {
perror("sigaction(SIGHUP)");
return -1;
}
return 0;
}
static int daemonize(void) {
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid > 0) {
// Parent process exits
exit(0);
}
// Child process continues
if (setsid() < 0) {
perror("setsid");
return -1;
}
// Change working directory to root
if (chdir("/") < 0) {
perror("chdir");
return -1;
}
// Close standard file descriptors
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
return 0;
}
static char *get_default_config_path(void) {
static char config_path[MAX_PATH_LEN];
const char *home = getenv("HOME");
const char *xdg_config = getenv("XDG_CONFIG_HOME");
if (xdg_config) {
snprintf(config_path, sizeof(config_path), "%s/chroma/%s", xdg_config,
CONFIG_FILE_NAME);
} else if (home) {
snprintf(config_path, sizeof(config_path), "%s/.config/chroma/%s", home,
CONFIG_FILE_NAME);
} else {
strcpy(config_path, CONFIG_FILE_NAME);
}
return config_path;
}
int main(int argc, char *argv[]) {
chroma_state_t state;
char *config_file = NULL;
bool daemon_mode = false;
bool verbose = false;
int opt;
int ret = 0;
static struct option long_options[] = {
{"config", required_argument, 0, 'c'}, {"daemon", no_argument, 0, 'd'},
{"verbose", no_argument, 0, 'v'}, {"help", no_argument, 0, 'h'},
{"version", no_argument, 0, 'V'}, {0, 0, 0, 0}};
// Parse command line arguments
while ((opt = getopt_long(argc, argv, "c:dvhV", long_options, NULL)) != -1) {
switch (opt) {
case 'c':
config_file = optarg;
break;
case 'd':
daemon_mode = true;
break;
case 'v':
verbose = true;
break;
case 'h':
print_usage(argv[0]);
return 0;
case 'V':
print_version();
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
// Initialize state
memset(&state, 0, sizeof(state));
state.config.daemon_mode = daemon_mode;
// Set log level based on verbose flag
if (verbose) {
chroma_set_log_level(1); // Enable debug logging
}
// Set up signal handlers
if (setup_signals() != 0) {
fprintf(stderr, "Failed to set up signal handlers\n");
return 1;
}
// Load configuration
if (!config_file) {
config_file = get_default_config_path();
}
// Daemonize if requested
if (daemon_mode) {
chroma_log("INFO", "Starting daemon mode");
if (daemonize() != 0) {
fprintf(stderr, "Failed to daemonize\n");
return 1;
}
}
// Initialize chroma
chroma_log("INFO", "Initializing chroma wallpaper daemon v%s",
CHROMA_VERSION);
ret = chroma_init(&state);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to initialize chroma: %s",
chroma_error_string(ret));
chroma_cleanup(&state);
return 1;
}
// Load configuration
chroma_log("INFO", "Loading configuration from: %s", config_file);
if (chroma_config_load(&state.config, config_file) != CHROMA_OK) {
chroma_log("WARN", "Failed to load config file, using defaults");
// Continue with default configuration
}
// Connect to Wayland
ret = chroma_wayland_connect(&state);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to connect to Wayland: %s",
chroma_error_string(ret));
chroma_cleanup(&state);
return 1;
}
// Initialize EGL
ret = chroma_egl_init(&state);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Failed to initialize EGL: %s",
chroma_error_string(ret));
chroma_cleanup(&state);
return 1;
}
chroma_log("INFO", "Chroma daemon initialized successfully");
// Main event loop
ret = chroma_run(&state);
if (ret != CHROMA_OK) {
chroma_log("ERROR", "Main loop failed: %s", chroma_error_string(ret));
}
// Cleanup
chroma_log("INFO", "Shutting down chroma daemon");
chroma_cleanup(&state);
return (ret == CHROMA_OK) ? 0 : 1;
}

431
src/render.c Normal file
View file

@ -0,0 +1,431 @@
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <EGL/egl.h>
#include <GLES2/gl2.h>
#include "../include/chroma.h"
// 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"
"void main() {\n"
" gl_Position = vec4(position, 0.0, 1.0);\n"
" v_texcoord = texcoord;\n"
"}\n";
// Fragment shader for simple texture rendering
static const char *fragment_shader_source =
"#version 120\n"
"varying vec2 v_texcoord;\n"
"uniform sampler2D texture;\n"
"void main() {\n"
" gl_FragColor = texture2D(texture, v_texcoord);\n"
"}\n";
// Vertices for a fullscreen quad
static const float vertices[] = {
// Position Texcoord
-1.0f, -1.0f, 0.0f, 1.0f, // Bottom left
1.0f, -1.0f, 1.0f, 1.0f, // Bottom right
1.0f, 1.0f, 1.0f, 0.0f, // Top right
-1.0f, 1.0f, 0.0f, 0.0f, // Top left
};
static const unsigned int indices[] = {
0, 1, 2, // First triangle
2, 3, 0 // Second triangle
};
// Shader compilation helper
static GLuint compile_shader(GLenum type, const char *source) {
GLuint shader = glCreateShader(type);
if (!shader) {
chroma_log("ERROR", "Failed to create shader");
return 0;
}
glShaderSource(shader, 1, &source, NULL);
glCompileShader(shader);
GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if (status != GL_TRUE) {
char log[512];
glGetShaderInfoLog(shader, sizeof(log), NULL, log);
chroma_log("ERROR", "Shader compilation failed: %s", log);
glDeleteShader(shader);
return 0;
}
return shader;
}
// Shader program creation helper
static GLuint create_shader_program(void) {
GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source);
if (!vertex_shader) {
return 0;
}
GLuint fragment_shader =
compile_shader(GL_FRAGMENT_SHADER, fragment_shader_source);
if (!fragment_shader) {
glDeleteShader(vertex_shader);
return 0;
}
GLuint program = glCreateProgram();
if (!program) {
chroma_log("ERROR", "Failed to create shader program");
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
return 0;
}
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if (status != GL_TRUE) {
char log[512];
glGetProgramInfoLog(program, sizeof(log), NULL, log);
chroma_log("ERROR", "Shader program linking failed: %s", log);
glDeleteProgram(program);
program = 0;
}
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
return program;
}
// EGL configuration selection
static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
EGLint attributes[] = {EGL_SURFACE_TYPE,
EGL_WINDOW_BIT,
EGL_RED_SIZE,
8,
EGL_GREEN_SIZE,
8,
EGL_BLUE_SIZE,
8,
EGL_ALPHA_SIZE,
8,
EGL_RENDERABLE_TYPE,
EGL_OPENGL_BIT,
EGL_NONE};
EGLint num_configs;
if (!eglChooseConfig(display, attributes, config, 1, &num_configs)) {
chroma_log("ERROR", "Failed to choose EGL config: 0x%04x", eglGetError());
return -1;
}
if (num_configs == 0) {
chroma_log("ERROR", "No suitable EGL configs found");
return -1;
}
return 0;
}
// EGL initialization
int chroma_egl_init(chroma_state_t *state) {
if (!state || !state->display) {
return CHROMA_ERROR_INIT;
}
// Get EGL display
state->egl_display = eglGetDisplay((EGLNativeDisplayType)state->display);
if (state->egl_display == EGL_NO_DISPLAY) {
chroma_log("ERROR", "Failed to get EGL display: 0x%04x", eglGetError());
return CHROMA_ERROR_EGL;
}
// Initialize EGL
EGLint major, minor;
if (!eglInitialize(state->egl_display, &major, &minor)) {
chroma_log("ERROR", "Failed to initialize EGL: 0x%04x", eglGetError());
return CHROMA_ERROR_EGL;
}
chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
// Bind OpenGL API
if (!eglBindAPI(EGL_OPENGL_API)) {
chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
chroma_egl_cleanup(state);
return CHROMA_ERROR_EGL;
}
// Choose EGL config
if (choose_egl_config(state->egl_display, &state->egl_config) != 0) {
chroma_egl_cleanup(state);
return CHROMA_ERROR_EGL;
}
// Create EGL context
EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
EGL_NO_CONTEXT, context_attributes);
if (state->egl_context == EGL_NO_CONTEXT) {
chroma_log("ERROR", "Failed to create EGL context: 0x%04x", eglGetError());
chroma_egl_cleanup(state);
return CHROMA_ERROR_EGL;
}
chroma_log("INFO", "EGL context created successfully");
return CHROMA_OK;
}
// EGL cleanup
void chroma_egl_cleanup(chroma_state_t *state) {
if (!state) {
return;
}
if (state->egl_display != EGL_NO_DISPLAY) {
eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
EGL_NO_CONTEXT);
if (state->egl_context != EGL_NO_CONTEXT) {
eglDestroyContext(state->egl_display, state->egl_context);
state->egl_context = EGL_NO_CONTEXT;
}
eglTerminate(state->egl_display);
state->egl_display = EGL_NO_DISPLAY;
}
chroma_log("INFO", "EGL cleaned up");
}
// Create surface for output
int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
if (!state || !output || !state->compositor || !state->layer_shell) {
return CHROMA_ERROR_INIT;
}
// Create Wayland surface
output->surface = wl_compositor_create_surface(state->compositor);
if (!output->surface) {
chroma_log("ERROR", "Failed to create Wayland surface for output %u",
output->id);
return CHROMA_ERROR_WAYLAND;
}
// Create layer surface for wallpaper
output->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
state->layer_shell, output->surface, output->wl_output,
ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND, "chroma-wallpaper");
if (!output->layer_surface) {
chroma_log("ERROR", "Failed to create layer surface for output %u",
output->id);
chroma_surface_destroy(output);
return CHROMA_ERROR_WAYLAND;
}
// Configure layer surface
zwlr_layer_surface_v1_set_size(output->layer_surface, output->width,
output->height);
zwlr_layer_surface_v1_set_anchor(output->layer_surface,
ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM |
ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT);
zwlr_layer_surface_v1_set_exclusive_zone(output->layer_surface, -1);
zwlr_layer_surface_v1_set_keyboard_interactivity(
output->layer_surface, ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE);
// Add layer surface listener
zwlr_layer_surface_v1_add_listener(
output->layer_surface, &chroma_layer_surface_listener_impl, output);
// Commit surface to trigger configure event
wl_surface_commit(output->surface);
// Wait for configure event
wl_display_roundtrip(state->display);
// Create EGL window
output->egl_window =
wl_egl_window_create(output->surface, output->width, output->height);
if (!output->egl_window) {
chroma_log("ERROR", "Failed to create EGL window for output %u",
output->id);
chroma_surface_destroy(output);
return CHROMA_ERROR_EGL;
}
// Create EGL surface
output->egl_surface =
eglCreateWindowSurface(state->egl_display, state->egl_config,
(EGLNativeWindowType)output->egl_window, NULL);
if (output->egl_surface == EGL_NO_SURFACE) {
chroma_log("ERROR", "Failed to create EGL surface for output %u: 0x%04x",
output->id, eglGetError());
chroma_surface_destroy(output);
return CHROMA_ERROR_EGL;
}
chroma_log("INFO", "Created surface for output %u (%dx%d)", output->id,
output->width, output->height);
return CHROMA_OK;
}
// Destroy surface
void chroma_surface_destroy(chroma_output_t *output) {
if (!output) {
return;
}
if (output->egl_surface != EGL_NO_SURFACE) {
eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
output->egl_surface = EGL_NO_SURFACE;
}
if (output->egl_window) {
wl_egl_window_destroy(output->egl_window);
output->egl_window = NULL;
}
if (output->layer_surface) {
zwlr_layer_surface_v1_destroy(output->layer_surface);
output->layer_surface = NULL;
}
if (output->surface) {
wl_surface_destroy(output->surface);
output->surface = NULL;
}
chroma_log("DEBUG", "Destroyed surface for output %u", output->id);
}
// Create texture from image data
static GLuint create_texture_from_image(chroma_image_t *image) {
if (!image || !image->loaded || !image->data) {
return 0;
}
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// Set texture parameters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// Upload texture data
GLenum format = (image->channels == 4) ? GL_RGBA : GL_RGB;
glTexImage2D(GL_TEXTURE_2D, 0, format, image->width, image->height, 0, format,
GL_UNSIGNED_BYTE, image->data);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
// Render wallpaper to output
int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
if (!state || !output || !output->image || !output->image->loaded) {
return CHROMA_ERROR_INIT;
}
// Make context current
if (!eglMakeCurrent(state->egl_display, output->egl_surface,
output->egl_surface, state->egl_context)) {
chroma_log("ERROR", "Failed to make EGL context current: 0x%04x",
eglGetError());
return CHROMA_ERROR_EGL;
}
// Set viewport
glViewport(0, 0, output->width, output->height);
// Clear screen
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Create shader program (should be cached in real implementation)
GLuint program = create_shader_program();
if (!program) {
return CHROMA_ERROR_EGL;
}
// Use shader program
glUseProgram(program);
// Create and bind texture
GLuint texture = create_texture_from_image(output->image);
if (!texture) {
chroma_log("ERROR", "Failed to create texture for output %u", output->id);
glDeleteProgram(program);
return CHROMA_ERROR_EGL;
}
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture);
glUniform1i(glGetUniformLocation(program, "texture"), 0);
// Create vertex buffer objects (should be cached in real implementation)
GLuint vbo, ebo;
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW);
// Set vertex attributes
GLint position_attr = glGetAttribLocation(program, "position");
GLint texcoord_attr = glGetAttribLocation(program, "texcoord");
glEnableVertexAttribArray(position_attr);
glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *)0);
glEnableVertexAttribArray(texcoord_attr);
glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *)(2 * sizeof(float)));
// Draw
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// Cleanup
glDeleteBuffers(1, &vbo);
glDeleteBuffers(1, &ebo);
glDeleteTextures(1, &texture);
glDeleteProgram(program);
// Swap buffers
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", output->id);
return CHROMA_OK;
}

416
src/utils.c Normal file
View file

@ -0,0 +1,416 @@
#include <ctype.h>
#include <errno.h>
#include <libgen.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "../include/chroma.h"
// Global state pointer for signal handling
static chroma_state_t *g_state = NULL;
static char *g_config_file = NULL;
// Signal handler implementation
static void signal_handler_impl(int sig) {
switch (sig) {
case SIGTERM:
case SIGINT:
chroma_log("INFO", "Received signal %d (%s), shutting down gracefully", sig,
(sig == SIGTERM) ? "SIGTERM" : "SIGINT");
chroma_should_quit = 1;
if (g_state) {
g_state->running = false;
}
break;
case SIGHUP:
chroma_log("INFO", "Received SIGHUP, reloading configuration");
if (g_state && g_config_file) {
chroma_reload_config(g_state, g_config_file);
}
break;
case SIGPIPE:
// Ignore SIGPIPE - we'll handle broken pipes in read/write calls
break;
default:
chroma_log("WARN", "Received unexpected signal: %d", sig);
break;
}
}
// Set up signal handlers
void chroma_handle_signals(void) {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = signal_handler_impl;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
// Install signal handlers
if (sigaction(SIGTERM, &sa, NULL) == -1) {
chroma_log("ERROR", "Failed to install SIGTERM handler: %s",
strerror(errno));
}
if (sigaction(SIGINT, &sa, NULL) == -1) {
chroma_log("ERROR", "Failed to install SIGINT handler: %s",
strerror(errno));
}
if (sigaction(SIGHUP, &sa, NULL) == -1) {
chroma_log("ERROR", "Failed to install SIGHUP handler: %s",
strerror(errno));
}
// Ignore SIGPIPE
sa.sa_handler = SIG_IGN;
if (sigaction(SIGPIPE, &sa, NULL) == -1) {
chroma_log("ERROR", "Failed to ignore SIGPIPE: %s", strerror(errno));
}
chroma_log("DEBUG", "Signal handlers installed");
}
// Set global state for signal handling
void chroma_set_signal_state(chroma_state_t *state, const char *config_file) {
g_state = state;
free(g_config_file);
g_config_file = config_file ? strdup(config_file) : NULL;
}
// Clean up signal handling resources
void chroma_cleanup_signals(void) {
g_state = NULL;
free(g_config_file);
g_config_file = NULL;
}
// Expand tilde in path
char *chroma_expand_path(const char *path) {
if (!path) {
return NULL;
}
if (path[0] != '~') {
return strdup(path);
}
const char *home;
if (path[1] == '/' || path[1] == '\0') {
// ~/... or just ~
home = getenv("HOME");
if (!home) {
struct passwd *pw = getpwuid(getuid());
if (pw) {
home = pw->pw_dir;
}
}
if (!home) {
chroma_log("ERROR", "Could not determine home directory");
return strdup(path); // Return original path as fallback
}
size_t home_len = strlen(home);
size_t path_len = strlen(path);
char *expanded = malloc(home_len + path_len); // -1 for ~ +1 for \0
if (!expanded) {
chroma_log("ERROR", "Failed to allocate memory for path expansion");
return strdup(path);
}
strcpy(expanded, home);
if (path[1] == '/') {
strcat(expanded, path + 1);
}
return expanded;
} else {
// ~user/...
const char *slash = strchr(path, '/');
size_t user_len = slash ? (size_t)(slash - path - 1) : strlen(path) - 1;
char *username = malloc(user_len + 1);
if (!username) {
return strdup(path);
}
strncpy(username, path + 1, user_len);
username[user_len] = '\0';
struct passwd *pw = getpwnam(username);
if (!pw) {
chroma_log("ERROR", "User not found: %s", username);
free(username);
return strdup(path);
}
free(username);
size_t home_len = strlen(pw->pw_dir);
size_t remaining_len = slash ? strlen(slash) : 0;
char *expanded = malloc(home_len + remaining_len + 1);
if (!expanded) {
return strdup(path);
}
strcpy(expanded, pw->pw_dir);
if (slash) {
strcat(expanded, slash);
}
return expanded;
}
}
// Create directory recursively
int chroma_mkdir_recursive(const char *path, mode_t mode) {
if (!path) {
return -1;
}
char *path_copy = strdup(path);
if (!path_copy) {
return -1;
}
char *p = path_copy;
// Skip leading slashes
while (*p == '/') {
p++;
}
while (*p) {
// Find next slash
while (*p && *p != '/') {
p++;
}
if (*p) {
*p = '\0';
// Create directory
if (mkdir(path_copy, mode) == -1 && errno != EEXIST) {
chroma_log("ERROR", "Failed to create directory %s: %s", path_copy,
strerror(errno));
free(path_copy);
return -1;
}
*p = '/';
p++;
}
}
// Create final directory
if (mkdir(path_copy, mode) == -1 && errno != EEXIST) {
chroma_log("ERROR", "Failed to create directory %s: %s", path_copy,
strerror(errno));
free(path_copy);
return -1;
}
free(path_copy);
return 0;
}
// Get configuration directory
char *chroma_get_config_dir(void) {
const char *xdg_config = getenv("XDG_CONFIG_HOME");
if (xdg_config) {
char *config_dir = malloc(strlen(xdg_config) + strlen("/chroma") + 1);
if (config_dir) {
sprintf(config_dir, "%s/chroma", xdg_config);
return config_dir;
}
}
const char *home = getenv("HOME");
if (home) {
char *config_dir = malloc(strlen(home) + strlen("/.config/chroma") + 1);
if (config_dir) {
sprintf(config_dir, "%s/.config/chroma", home);
return config_dir;
}
}
return strdup("/etc/chroma"); // Fallback
}
// Check if path exists
bool chroma_path_exists(const char *path) {
if (!path) {
return false;
}
struct stat st;
return (stat(path, &st) == 0);
}
// Check if path is a regular file
bool chroma_is_regular_file(const char *path) {
if (!path) {
return false;
}
struct stat st;
if (stat(path, &st) != 0) {
return false;
}
return S_ISREG(st.st_mode);
}
// Check if path is a directory
bool chroma_is_directory(const char *path) {
if (!path) {
return false;
}
struct stat st;
if (stat(path, &st) != 0) {
return false;
}
return S_ISDIR(st.st_mode);
}
// Get file size
long chroma_get_file_size(const char *path) {
if (!path) {
return -1;
}
struct stat st;
if (stat(path, &st) != 0) {
return -1;
}
return st.st_size;
}
// Get file extension
const char *chroma_get_file_extension(const char *path) {
if (!path) {
return NULL;
}
const char *last_dot = strrchr(path, '.');
if (!last_dot || last_dot == path) {
return NULL;
}
return last_dot + 1;
}
// Case-insensitive string comparison
int chroma_strcasecmp(const char *s1, const char *s2) {
if (!s1 || !s2) {
return (s1 == s2) ? 0 : (s1 ? 1 : -1);
}
while (*s1 && *s2) {
int c1 = tolower((unsigned char)*s1);
int c2 = tolower((unsigned char)*s2);
if (c1 != c2) {
return c1 - c2;
}
s1++;
s2++;
}
return tolower((unsigned char)*s1) - tolower((unsigned char)*s2);
}
// Safe string copy
size_t chroma_strlcpy(char *dst, const char *src, size_t size) {
size_t src_len = strlen(src);
if (size > 0) {
size_t copy_len = (src_len < size - 1) ? src_len : size - 1;
memcpy(dst, src, copy_len);
dst[copy_len] = '\0';
}
return src_len;
}
// Safe string concatenation
size_t chroma_strlcat(char *dst, const char *src, size_t size) {
size_t dst_len = strnlen(dst, size);
size_t src_len = strlen(src);
if (dst_len < size) {
size_t copy_len = size - dst_len - 1;
if (src_len < copy_len) {
copy_len = src_len;
}
memcpy(dst + dst_len, src, copy_len);
dst[dst_len + copy_len] = '\0';
}
return dst_len + src_len;
}
// Get current time in milliseconds
long long chroma_get_time_ms(void) {
struct timeval tv;
if (gettimeofday(&tv, NULL) != 0) {
return 0;
}
return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
// Sleep for specified milliseconds
void chroma_sleep_ms(long ms) {
if (ms <= 0) {
return;
}
struct timespec ts;
ts.tv_sec = ms / 1000;
ts.tv_nsec = (ms % 1000) * 1000000;
nanosleep(&ts, NULL);
}
// Format memory size in human readable format
void chroma_format_memory_size(size_t bytes, char *buffer, size_t buffer_size) {
if (!buffer || buffer_size == 0) {
return;
}
const char *units[] = {"B", "KB", "MB", "GB", "TB"};
const int num_units = sizeof(units) / sizeof(units[0]);
double size = (double)bytes;
int unit_index = 0;
while (size >= 1024.0 && unit_index < num_units - 1) {
size /= 1024.0;
unit_index++;
}
if (unit_index == 0) {
snprintf(buffer, buffer_size, "%.0f %s", size, units[unit_index]);
} else {
snprintf(buffer, buffer_size, "%.2f %s", size, units[unit_index]);
}
}
// Cleanup utility functions
void chroma_utils_cleanup(void) { chroma_cleanup_signals(); }

402
src/wayland.c Normal file
View file

@ -0,0 +1,402 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "../include/chroma.h"
// Registry listener
static void registry_global(void *data, struct wl_registry *registry,
uint32_t id, const char *interface,
uint32_t version) {
chroma_state_t *state = (chroma_state_t *)data;
chroma_log("DEBUG", "Registry global: %s (id=%u, version=%u)", interface, id,
version);
if (strcmp(interface, wl_compositor_interface.name) == 0) {
state->compositor = wl_registry_bind(registry, id, &wl_compositor_interface,
version < 4 ? version : 4);
if (!state->compositor) {
chroma_log("ERROR", "Failed to bind compositor");
} else {
chroma_log("INFO", "Bound compositor (version %u)", version);
}
} else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) {
state->layer_shell =
wl_registry_bind(registry, id, &zwlr_layer_shell_v1_interface,
version < 4 ? version : 4);
if (!state->layer_shell) {
chroma_log("ERROR", "Failed to bind layer shell");
} else {
chroma_log("INFO", "Bound layer shell (version %u)", version);
}
} else if (strcmp(interface, wl_output_interface.name) == 0) {
struct wl_output *output = wl_registry_bind(
registry, id, &wl_output_interface, version < 4 ? version : 4);
if (!output) {
chroma_log("ERROR", "Failed to bind output %u", id);
return;
}
if (chroma_output_add(state, id, output) != CHROMA_OK) {
chroma_log("ERROR", "Failed to add output %u", id);
wl_output_destroy(output);
} else {
chroma_log("INFO", "Added output %u", id);
}
}
}
static void registry_global_remove(void *data, struct wl_registry *registry,
uint32_t id) {
chroma_state_t *state = (chroma_state_t *)data;
(void)registry; // Unused parameter
chroma_log("DEBUG", "Registry global remove: id=%u", id);
chroma_output_remove(state, id);
}
const struct wl_registry_listener chroma_registry_listener_impl = {
.global = registry_global,
.global_remove = registry_global_remove,
};
/* Layer surface event handlers */
static void layer_surface_configure(void *data,
struct zwlr_layer_surface_v1 *layer_surface,
uint32_t serial, uint32_t width,
uint32_t height) {
chroma_output_t *output = (chroma_output_t *)data;
(void)layer_surface; // Unused parameter
chroma_log("DEBUG", "Layer surface configure: %ux%u, serial=%u", width,
height, serial);
output->configure_serial = serial;
/* Acknowledge the configure event */
zwlr_layer_surface_v1_ack_configure(output->layer_surface, serial);
/* Commit the surface to apply the acknowledgment */
wl_surface_commit(output->surface);
chroma_log("DEBUG", "Acknowledged layer surface configure for output %u",
output->id);
}
static void layer_surface_closed(void *data,
struct zwlr_layer_surface_v1 *layer_surface) {
chroma_output_t *output = (chroma_output_t *)data;
(void)layer_surface; /* Unused parameter */
chroma_log("INFO", "Layer surface closed for output %u", output->id);
/* Clean up the surface */
if (output->surface) {
chroma_surface_destroy(output);
}
}
const struct zwlr_layer_surface_v1_listener chroma_layer_surface_listener_impl =
{
.configure = layer_surface_configure,
.closed = layer_surface_closed,
};
/* Output event handlers */
static void output_geometry(void *data, struct wl_output *output, int32_t x,
int32_t y, int32_t physical_width,
int32_t physical_height, int32_t subpixel,
const char *make, const char *model,
int32_t transform) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; // Unused parameter
(void)subpixel; // Unused parameter
(void)make; // Unused parameter
(void)model; // Unused parameter
chroma_output->x = x;
chroma_output->y = y;
chroma_output->transform = transform;
chroma_log("DEBUG", "Output %u geometry: %dx%d at (%d,%d), transform=%d",
chroma_output->id, physical_width, physical_height, x, y,
transform);
}
static void output_mode(void *data, struct wl_output *output, uint32_t flags,
int32_t width, int32_t height, int32_t refresh) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; // Unused parameter
if (flags & WL_OUTPUT_MODE_CURRENT) {
chroma_output->width = width;
chroma_output->height = height;
chroma_log("DEBUG", "Output %u mode: %dx%d@%d (current)", chroma_output->id,
width, height, refresh);
}
}
static void output_scale(void *data, struct wl_output *output, int32_t scale) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; // Unused parameter
chroma_output->scale = scale;
chroma_log("DEBUG", "Output %u scale: %d", chroma_output->id, scale);
}
static void output_name(void *data, struct wl_output *output,
const char *name) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; // Unused parameter
free(chroma_output->name);
chroma_output->name = strdup(name);
if (!chroma_output->name) {
chroma_log("ERROR", "Failed to allocate memory for output name");
return;
}
chroma_log("DEBUG", "Output %u name: %s", chroma_output->id, name);
}
static void output_description(void *data, struct wl_output *output,
const char *description) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; // Unused parameter
free(chroma_output->description);
chroma_output->description = strdup(description);
if (!chroma_output->description) {
chroma_log("ERROR", "Failed to allocate memory for output description");
return;
}
chroma_log("DEBUG", "Output %u description: %s", chroma_output->id,
description);
}
static void output_done(void *data, struct wl_output *output) {
chroma_output_t *chroma_output = (chroma_output_t *)data;
(void)output; /* Unused parameter */
chroma_log("DEBUG", "Output %u done - configuration complete",
chroma_output->id);
// Mark output as active and ready for wallpaper assignment
chroma_output->active = true;
// Trigger wallpaper assignment for this output
if (chroma_output->state) {
handle_output_done(chroma_output->state, chroma_output);
}
}
const struct wl_output_listener chroma_output_listener_impl = {
.geometry = output_geometry,
.mode = output_mode,
.scale = output_scale,
.name = output_name,
.description = output_description,
.done = output_done,
};
/* Wayland connection functions */
int chroma_wayland_connect(chroma_state_t *state) {
if (!state) {
return CHROMA_ERROR_INIT;
}
// Connect to Wayland display
state->display = wl_display_connect(NULL);
if (!state->display) {
chroma_log("ERROR", "Failed to connect to Wayland display: %s",
strerror(errno));
return CHROMA_ERROR_WAYLAND;
}
// Get registry
state->registry = wl_display_get_registry(state->display);
if (!state->registry) {
chroma_log("ERROR", "Failed to get Wayland registry");
chroma_wayland_disconnect(state);
return CHROMA_ERROR_WAYLAND;
}
// Add registry listener
wl_registry_add_listener(state->registry, &chroma_registry_listener_impl,
state);
// Roundtrip to get all globals
if (wl_display_roundtrip(state->display) == -1) {
chroma_log("ERROR", "Failed to roundtrip Wayland display");
chroma_wayland_disconnect(state);
return CHROMA_ERROR_WAYLAND;
}
// Check if we got a compositor
if (!state->compositor) {
chroma_log("ERROR", "No compositor available");
chroma_wayland_disconnect(state);
return CHROMA_ERROR_WAYLAND;
}
// Check if we got layer shell
if (!state->layer_shell) {
chroma_log("ERROR", "No layer shell available - compositor may not support "
"wlr-layer-shell");
chroma_wayland_disconnect(state);
return CHROMA_ERROR_WAYLAND;
}
chroma_log("INFO", "Connected to Wayland display, found %d outputs",
state->output_count);
return CHROMA_OK;
}
void chroma_wayland_disconnect(chroma_state_t *state) {
if (!state) {
return;
}
// Clean up all outputs
for (int i = 0; i < state->output_count; i++) {
chroma_output_t *output = &state->outputs[i];
if (output->surface) {
chroma_surface_destroy(output);
}
if (output->wl_output) {
wl_output_destroy(output->wl_output);
}
free(output->name);
free(output->description);
}
state->output_count = 0;
// Clean up Wayland objects
if (state->layer_shell) {
zwlr_layer_shell_v1_destroy(state->layer_shell);
state->layer_shell = NULL;
}
if (state->compositor) {
wl_compositor_destroy(state->compositor);
state->compositor = NULL;
}
if (state->registry) {
wl_registry_destroy(state->registry);
state->registry = NULL;
}
if (state->display) {
wl_display_disconnect(state->display);
state->display = NULL;
}
chroma_log("INFO", "Disconnected from Wayland display");
}
/* Output management functions */
int chroma_output_add(chroma_state_t *state, uint32_t id,
struct wl_output *output) {
if (!state || !output) {
return CHROMA_ERROR_INIT;
}
if (state->output_count >= MAX_OUTPUTS) {
chroma_log("ERROR", "Maximum number of outputs (%d) exceeded", MAX_OUTPUTS);
return CHROMA_ERROR_MEMORY;
}
chroma_output_t *chroma_output = &state->outputs[state->output_count];
memset(chroma_output, 0, sizeof(chroma_output_t));
chroma_output->wl_output = output;
chroma_output->id = id;
chroma_output->scale = 1; // Default scale
chroma_output->active = false;
chroma_output->state = state;
// Add output listener
wl_output_add_listener(output, &chroma_output_listener_impl, chroma_output);
state->output_count++;
chroma_log("INFO", "Added output %u (total: %d)", id, state->output_count);
return CHROMA_OK;
}
void chroma_output_remove(chroma_state_t *state, uint32_t id) {
if (!state) {
return;
}
chroma_output_t *output = chroma_output_find_by_id(state, id);
if (!output) {
chroma_log("WARN", "Attempted to remove non-existent output %u", id);
return;
}
chroma_log("INFO", "Removing output %u (%s)", id,
output->name ? output->name : "unknown");
/* Clean up surface if it exists */
if (output->surface) {
chroma_surface_destroy(output);
}
/* Clean up Wayland output */
if (output->wl_output) {
wl_output_destroy(output->wl_output);
}
/* Free allocated strings */
free(output->name);
free(output->description);
/* Remove from array by shifting remaining elements */
int index = output - state->outputs;
int remaining = state->output_count - index - 1;
if (remaining > 0) {
memmove(output, output + 1, remaining * sizeof(chroma_output_t));
}
state->output_count--;
chroma_log("INFO", "Removed output %u (remaining: %d)", id,
state->output_count);
}
chroma_output_t *chroma_output_find_by_id(chroma_state_t *state, uint32_t id) {
if (!state) {
return NULL;
}
for (int i = 0; i < state->output_count; i++) {
if (state->outputs[i].id == id) {
return &state->outputs[i];
}
}
return NULL;
}
chroma_output_t *chroma_output_find_by_name(chroma_state_t *state,
const char *name) {
if (!state || !name) {
return NULL;
}
for (int i = 0; i < state->output_count; i++) {
chroma_output_t *output = &state->outputs[i];
if (output->name && strcmp(output->name, name) == 0) {
return output;
}
}
return NULL;
}