initial commit
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I6a6a6964ee9e6ebe436ca8328c6e4a7ec7c9d8d4
This commit is contained in:
		
				commit
				
					
						fcc080871a
					
				
			
		
					 11 changed files with 12536 additions and 0 deletions
				
			
		
							
								
								
									
										222
									
								
								include/chroma.h
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								include/chroma.h
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										7988
									
								
								include/stb_image.h
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										391
									
								
								protocols/wlr-layer-shell-unstable-v1.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										391
									
								
								protocols/wlr-layer-shell-unstable-v1.xml
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										1415
									
								
								protocols/xdg-shell.xml
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										326
									
								
								src/config.c
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								src/config.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										439
									
								
								src/core.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										268
									
								
								src/image.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										238
									
								
								src/main.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										431
									
								
								src/render.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										416
									
								
								src/utils.c
									
										
									
									
									
										Normal 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
									
								
							
							
						
						
									
										402
									
								
								src/wayland.c
									
										
									
									
									
										Normal 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; | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue