/* * oledsaver -- combined OLED screensaver: manager + renderer in one binary. * * No flags → manager mode (idle detection, Wayfire focus IPC, Wayland outputs) * --render → renderer mode (Wayland layer-shell Matrix rain) * * The manager fork/execs itself with --render --output [--color ...] * [--font-size ...] [--min-speed ...] [--max-speed ...] etc. using /proc/self/exe. * * Build: make -C ~/.local/src oledsaver * Requires: json-c, pthreads, wayland-client, wayland-egl, EGL, GLESv2, freetype2 */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include FT_FREETYPE_H #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" #include "ext-idle-notify-v1-client-protocol.h" /* =================================================================== * RENDERER MODE (Wayland layer-shell Matrix rain) * =================================================================== */ /* ── Tuning constants ─────────────────────────────────────────────── */ #define TARGET_FPS 30 #define FRAME_INTERVAL_US (1000000 / TARGET_FPS) /* These are now defaults; actual values come from CLI flags */ #define DEFAULT_MIN_DENSITY 0.25 #define DEFAULT_MAX_DENSITY 0.45 #define DEFAULT_MIN_TRAIL 4 #define DEFAULT_MAX_TRAIL 28 #define DEFAULT_MIN_SPEED 0.4 #define DEFAULT_MAX_SPEED 2.0 #define DEFAULT_AVG_SPEED 0.8 #define MUTATION_CHANCE 0.07 #define RESPAWN_DELAY_MAX 40 #define FADE_LEVELS 6 #define HEAD_GLOW_EXTRA 2 #define DEFAULT_DEPTH_LAYERS 4 #define MIN_DEPTH_LAYERS 1 #define MAX_DEPTH_LAYERS 8 /* Character sets */ static const uint32_t KATAKANA_START = 0xFF66; static const uint32_t KATAKANA_END = 0xFF9D; #define NUM_KATAKANA (KATAKANA_END - KATAKANA_START + 1) static const char *LATIN_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; static const char *DIGIT_CHARS = "0123456789"; static const char *SYMBOL_CHARS = "!@#$%^&*()-=+[]{}|;:<>?/~"; /* ── Color presets ────────────────────────────────────────────────── */ typedef struct { float r, g, b; } rgb_t; static const struct { const char *name; rgb_t color; } RENDER_COLOR_TABLE[] = { { "green", { 0.0f, 1.0f, 0.0f } }, { "red", { 1.0f, 0.2f, 0.2f } }, { "blue", { 0.3f, 0.5f, 1.0f } }, { "white", { 1.0f, 1.0f, 1.0f } }, { "yellow", { 1.0f, 1.0f, 0.0f } }, { "cyan", { 0.0f, 1.0f, 1.0f } }, { "magenta", { 1.0f, 0.0f, 1.0f } }, }; #define RENDER_NUM_COLORS (sizeof(RENDER_COLOR_TABLE) / sizeof(RENDER_COLOR_TABLE[0])) /* ── Glyph atlas types ────────────────────────────────────────────── */ #define ATLAS_MAX_GLYPHS 256 typedef struct { uint32_t codepoint; float u0, v0, u1, v1; /* UV coords in atlas */ int width, height; /* glyph bitmap size */ int bearing_x, bearing_y; int advance; /* horizontal advance in pixels (26.6 fixed >> 6) */ } glyph_info_t; typedef struct { GLuint texture; int tex_width, tex_height; glyph_info_t glyphs[ATLAS_MAX_GLYPHS]; int glyph_count; int cell_w, cell_h; /* max advance and line height for grid calc */ } glyph_atlas_t; /* ── Data types (renderer) ────────────────────────────────────────── */ typedef struct { uint32_t codepoint; int age; bool mirrored; /* horizontally flip the glyph */ } render_cell_t; typedef struct { bool active; double speed; double head_pos; int trail_len; int respawn_timer; float x_offset; /* horizontal jitter in pixels */ bool skip; /* permanently inactive column for gaps */ int flash_timer; /* when > 0, column is flashing */ float flash_brightness; /* brightness boost during flash */ rgb_t color; /* per-trail color */ } render_column_t; typedef struct { int cols, rows; int cell_w, cell_h; double opacity; double speed_scale; double density; double font_scale; render_column_t *columns; render_cell_t *cells; } depth_layer_t; /* Depth layer extreme values for interpolation */ #define LAYER_FAR_FONT_SCALE 0.5 #define LAYER_FAR_OPACITY 0.2 #define LAYER_FAR_SPEED_SCALE 0.4 #define LAYER_FAR_DENSITY 0.35 #define LAYER_NEAR_FONT_SCALE 1.25 #define LAYER_NEAR_OPACITY 1.0 #define LAYER_NEAR_SPEED_SCALE 1.4 #define LAYER_NEAR_DENSITY 0.15 /* ── Vertex type for batched quad rendering ───────────────────────── */ typedef struct { float x, y; float u, v; float r, g, b, a; } vertex_t; /* Max quads per draw call (each quad = 6 vertices) */ #define MAX_QUADS (256 * 256) #define VERTS_PER_QUAD 6 /* ── Render phase lifecycle ────────────────────────────────────────── */ typedef enum { PHASE_FADE_TO_BLACK, /* desktop fades to black overlay */ PHASE_FADE_IN, /* matrix rain fades in on black */ PHASE_RUNNING, /* normal matrix rain */ PHASE_FADE_OUT, /* matrix rain fades out to black */ PHASE_FADE_TO_DESKTOP /* black fades to reveal desktop */ } render_phase_t; /* ── Renderer global state ────────────────────────────────────────── */ static struct { /* Wayland globals */ struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; struct zwlr_layer_shell_v1 *layer_shell; struct zxdg_output_manager_v1 *xdg_output_manager; /* Target output */ struct wl_output *target_output; char target_name[128]; bool output_found; int output_width; int output_height; int output_scale; /* Surface */ struct wl_surface *surface; struct zwlr_layer_surface_v1 *layer_surface; uint32_t configure_serial; bool configured; int surface_width; int surface_height; /* EGL */ EGLDisplay egl_display; EGLContext egl_context; EGLSurface egl_surface; struct wl_egl_window *egl_window; /* OpenGL */ GLuint shader_program; GLint u_projection; GLint u_atlas; GLint a_pos; GLint a_uv; GLint a_color; GLuint vbo; vertex_t *vertex_buf; int vertex_count; /* FreeType */ FT_Library ft_library; FT_Face ft_face; /* Glyph atlas (one at largest font size) */ glyph_atlas_t atlas; /* Matrix state */ int num_depth_layers; depth_layer_t layers[MAX_DEPTH_LAYERS]; rgb_t base_color; int color_mode; /* 0=fixed, 1=random_trail_named, 2=random_trail_hex */ int font_size; /* Configurable ranges (set via CLI flags) */ double min_speed; double max_speed; double avg_speed; int min_trail; int max_trail; double min_density; double max_density; double layer_near_density; double layer_far_density; /* Run control */ volatile sig_atomic_t stop; volatile sig_atomic_t fade_out_requested; bool closed; /* Phase-based transition state */ render_phase_t phase; int phase_frame; float surface_alpha; /* alpha for glClearColor (0=transparent, 1=opaque) */ float fade_opacity; /* rain opacity multiplier (0=invisible, 1=full) */ int fade_frames; /* configurable transition duration */ } S; #define DEFAULT_FADE_FRAMES 30 /* 1 second at 30fps */ /* ── Renderer helpers ─────────────────────────────────────────────── */ static inline double render_randf(void) { return (double)rand() / RAND_MAX; } static inline int render_randi(int lo, int hi) { return lo + rand() % (hi - lo + 1); } /* Generate a random speed biased toward avg, clamped to [lo, hi]. Uses sum-of-3-uniforms (approx gaussian) for bell-curve distribution. */ static inline double render_rand_speed(double lo, double hi, double avg) { double r = (render_randf() + render_randf() + render_randf()) / 3.0; /* r is ~gaussian centered on 0.5; map to [lo, hi] biased toward avg */ double v = (r < 0.5) ? lo + (avg - lo) * (r / 0.5) : avg + (hi - avg) * ((r - 0.5) / 0.5); if (v < lo) v = lo; if (v > hi) v = hi; return v; } static render_cell_t *render_cell_at_layer(depth_layer_t *layer, int row, int col) { return &layer->cells[row * layer->cols + col]; } static uint32_t render_random_codepoint(void) { int choice = rand() % 100; if (choice < 40) { return KATAKANA_START + (rand() % NUM_KATAKANA); } else if (choice < 70) { return (uint32_t)LATIN_CHARS[rand() % (int)strlen(LATIN_CHARS)]; } else if (choice < 85) { return (uint32_t)DIGIT_CHARS[rand() % (int)strlen(DIGIT_CHARS)]; } else { return (uint32_t)SYMBOL_CHARS[rand() % (int)strlen(SYMBOL_CHARS)]; } } static rgb_t render_parse_hex(const char *hex) { rgb_t c = { 0.0f, 0.0f, 0.0f }; const char *p = hex; if (*p == '#') p++; unsigned int val = 0; if (sscanf(p, "%06x", &val) == 1) { c.r = ((val >> 16) & 0xFF) / 255.0f; c.g = ((val >> 8) & 0xFF) / 255.0f; c.b = (val & 0xFF) / 255.0f; } return c; } static rgb_t render_lookup_color(const char *name) { /* Hex color: #RRGGBB or RRGGBB */ if (name[0] == '#' || (strlen(name) == 6 && strspn(name, "0123456789abcdefABCDEF") == 6)) return render_parse_hex(name); for (size_t i = 0; i < RENDER_NUM_COLORS; i++) { if (strcasecmp(name, RENDER_COLOR_TABLE[i].name) == 0) return RENDER_COLOR_TABLE[i].color; } /* Per-output random: pick one color at startup */ if (strcasecmp(name, "random_named") == 0) return RENDER_COLOR_TABLE[rand() % RENDER_NUM_COLORS].color; if (strcasecmp(name, "random_hex") == 0) return (rgb_t){ (float)(rand() % 256) / 255.0f, (float)(rand() % 256) / 255.0f, (float)(rand() % 256) / 255.0f }; /* Per-trail random modes: base_color is fallback, actual color set per-trail */ if (strcasecmp(name, "random_trail_named") == 0 || strcasecmp(name, "random_trail_hex") == 0) return RENDER_COLOR_TABLE[0].color; return RENDER_COLOR_TABLE[0].color; } enum { COLOR_FIXED = 0, COLOR_RANDOM = 1, COLOR_RANDOM_HEX = 2 }; static rgb_t render_random_color(void) { return RENDER_COLOR_TABLE[rand() % RENDER_NUM_COLORS].color; } static rgb_t render_random_hex_color(void) { return (rgb_t){ (float)(rand() % 256) / 255.0f, (float)(rand() % 256) / 255.0f, (float)(rand() % 256) / 255.0f }; } static rgb_t render_trail_color(void) { switch (S.color_mode) { case COLOR_RANDOM: return render_random_color(); case COLOR_RANDOM_HEX: return render_random_hex_color(); default: return S.base_color; } } /* ── FreeType / Glyph Atlas ───────────────────────────────────────── */ /* Build list of all codepoints we need */ static int render_build_codepoint_list(uint32_t *out, int max) { int n = 0; /* Katakana */ for (uint32_t cp = KATAKANA_START; cp <= KATAKANA_END && n < max; cp++) out[n++] = cp; /* Latin */ for (const char *p = LATIN_CHARS; *p && n < max; p++) out[n++] = (uint32_t)*p; /* Digits */ for (const char *p = DIGIT_CHARS; *p && n < max; p++) out[n++] = (uint32_t)*p; /* Symbols */ for (const char *p = SYMBOL_CHARS; *p && n < max; p++) out[n++] = (uint32_t)*p; return n; } static bool render_init_freetype(int font_pixel_size) { if (FT_Init_FreeType(&S.ft_library)) { fprintf(stderr, "Failed to init FreeType\n"); return false; } /* Try fonts in order of preference */ static const char *font_paths[] = { "/usr/share/fonts/noto-cjk/NotoSansMonoCJKjp-Regular.otf", "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc", "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc", "/usr/share/fonts/google-noto-cjk/NotoSansMonoCJKjp-Regular.otf", "/usr/share/fonts/noto/NotoSansMono-Regular.ttf", "/usr/share/fonts/liberation-mono/LiberationMono-Regular.ttf", "/usr/share/fonts/liberation/LiberationMono-Regular.ttf", "/usr/share/fonts/TTF/LiberationMono-Regular.ttf", "/usr/share/fonts/dejavu/DejaVuSansMono.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", NULL }; bool loaded = false; for (int i = 0; font_paths[i]; i++) { if (FT_New_Face(S.ft_library, font_paths[i], 0, &S.ft_face) == 0) { fprintf(stderr, "Loaded font: %s\n", font_paths[i]); loaded = true; break; } } if (!loaded) { fprintf(stderr, "No suitable font found, trying fc-match\n"); /* Last resort: use fontconfig via popen */ FILE *fp = popen("fc-match -f '%{file}' 'monospace'", "r"); if (fp) { char path[512]; if (fgets(path, sizeof(path), fp)) { char *nl = strchr(path, '\n'); if (nl) *nl = '\0'; if (FT_New_Face(S.ft_library, path, 0, &S.ft_face) == 0) { fprintf(stderr, "Loaded font via fc-match: %s\n", path); loaded = true; } } pclose(fp); } } if (!loaded) { fprintf(stderr, "Could not load any font\n"); FT_Done_FreeType(S.ft_library); return false; } FT_Set_Pixel_Sizes(S.ft_face, 0, font_pixel_size); return true; } static bool render_build_atlas(int font_pixel_size) { uint32_t codepoints[ATLAS_MAX_GLYPHS]; int cp_count = render_build_codepoint_list(codepoints, ATLAS_MAX_GLYPHS); /* First pass: render all glyphs to measure total atlas size */ FT_Set_Pixel_Sizes(S.ft_face, 0, font_pixel_size); /* Estimate atlas dimensions: pack in rows */ int row_height = 0; int atlas_w = 0, atlas_h = 0; int padding = 2; int max_row_w = 2048; /* max texture width */ /* Temporary storage for bitmaps */ typedef struct { uint32_t cp; int w, h, bearing_x, bearing_y, advance; unsigned char *bitmap; } tmp_glyph_t; tmp_glyph_t *tmp_glyphs = calloc(cp_count, sizeof(tmp_glyph_t)); int valid_count = 0; int max_cell_w = 0, max_cell_h = 0; for (int i = 0; i < cp_count; i++) { if (FT_Load_Char(S.ft_face, codepoints[i], FT_LOAD_RENDER)) { continue; /* skip glyphs that fail to render */ } FT_GlyphSlot g = S.ft_face->glyph; int gw = g->bitmap.width; int gh = g->bitmap.rows; tmp_glyph_t *tg = &tmp_glyphs[valid_count]; tg->cp = codepoints[i]; tg->w = gw; tg->h = gh; tg->bearing_x = g->bitmap_left; tg->bearing_y = g->bitmap_top; tg->advance = (int)(g->advance.x >> 6); if (tg->advance > max_cell_w) max_cell_w = tg->advance; if (gh > max_cell_h) max_cell_h = gh; /* Copy bitmap data */ tg->bitmap = malloc(gw * gh); if (tg->bitmap && gw > 0 && gh > 0) { for (int r = 0; r < gh; r++) memcpy(tg->bitmap + r * gw, g->bitmap.buffer + r * g->bitmap.pitch, gw); } valid_count++; } /* Calculate atlas layout */ int cx = 0, cy = 0; row_height = 0; for (int i = 0; i < valid_count; i++) { tmp_glyph_t *tg = &tmp_glyphs[i]; int gw = tg->w + padding; int gh = tg->h + padding; if (cx + gw > max_row_w) { cy += row_height; cx = 0; row_height = 0; } if (gh > row_height) row_height = gh; cx += gw; if (cx > atlas_w) atlas_w = cx; } atlas_h = cy + row_height; /* Round up to power of 2 */ int pot_w = 1; while (pot_w < atlas_w) pot_w <<= 1; int pot_h = 1; while (pot_h < atlas_h) pot_h <<= 1; atlas_w = pot_w; atlas_h = pot_h; /* Allocate atlas pixel data (single channel) */ unsigned char *atlas_data = calloc(atlas_w * atlas_h, 1); /* Second pass: blit glyphs and record UVs */ cx = 0; cy = 0; row_height = 0; S.atlas.glyph_count = 0; for (int i = 0; i < valid_count; i++) { tmp_glyph_t *tg = &tmp_glyphs[i]; int gw = tg->w; int gh = tg->h; if (cx + gw + padding > atlas_w) { cy += row_height; cx = 0; row_height = 0; } if (gh + padding > row_height) row_height = gh + padding; /* Blit */ if (tg->bitmap && gw > 0 && gh > 0) { for (int r = 0; r < gh; r++) { memcpy(atlas_data + (cy + r) * atlas_w + cx, tg->bitmap + r * gw, gw); } } /* Record glyph info */ glyph_info_t *gi = &S.atlas.glyphs[S.atlas.glyph_count++]; gi->codepoint = tg->cp; gi->u0 = (float)cx / atlas_w; gi->v0 = (float)cy / atlas_h; gi->u1 = (float)(cx + gw) / atlas_w; gi->v1 = (float)(cy + gh) / atlas_h; gi->width = gw; gi->height = gh; gi->bearing_x = tg->bearing_x; gi->bearing_y = tg->bearing_y; gi->advance = tg->advance; cx += gw + padding; free(tg->bitmap); } free(tmp_glyphs); S.atlas.tex_width = atlas_w; S.atlas.tex_height = atlas_h; /* Use font metrics for consistent cell size */ S.atlas.cell_w = max_cell_w > 0 ? max_cell_w : font_pixel_size; /* Use ascender + descender for proper line height */ int line_height = (int)((S.ft_face->size->metrics.height + 63) >> 6); S.atlas.cell_h = line_height > 0 ? line_height : (int)(font_pixel_size * 1.4); if (max_cell_h > S.atlas.cell_h) S.atlas.cell_h = max_cell_h; /* Create GL texture */ glGenTextures(1, &S.atlas.texture); glBindTexture(GL_TEXTURE_2D, S.atlas.texture); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, atlas_w, atlas_h, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, atlas_data); free(atlas_data); fprintf(stderr, "Atlas: %dx%d, %d glyphs, cell=%dx%d\n", atlas_w, atlas_h, S.atlas.glyph_count, S.atlas.cell_w, S.atlas.cell_h); return true; } static const glyph_info_t *render_find_glyph(uint32_t codepoint) { for (int i = 0; i < S.atlas.glyph_count; i++) { if (S.atlas.glyphs[i].codepoint == codepoint) return &S.atlas.glyphs[i]; } /* Fallback to '?' or first glyph */ for (int i = 0; i < S.atlas.glyph_count; i++) { if (S.atlas.glyphs[i].codepoint == '?') return &S.atlas.glyphs[i]; } return S.atlas.glyph_count > 0 ? &S.atlas.glyphs[0] : NULL; } /* ── EGL setup ────────────────────────────────────────────────────── */ static bool render_init_egl(void) { S.egl_display = eglGetDisplay((EGLNativeDisplayType)S.display); if (S.egl_display == EGL_NO_DISPLAY) { /* Try platform extension */ PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); if (eglGetPlatformDisplayEXT) { S.egl_display = eglGetPlatformDisplayEXT( EGL_PLATFORM_WAYLAND_KHR, S.display, NULL); } } if (S.egl_display == EGL_NO_DISPLAY) { fprintf(stderr, "Failed to get EGL display\n"); return false; } EGLint major, minor; if (!eglInitialize(S.egl_display, &major, &minor)) { fprintf(stderr, "Failed to initialize EGL\n"); return false; } fprintf(stderr, "EGL %d.%d initialized\n", major, minor); if (!eglBindAPI(EGL_OPENGL_ES_API)) { fprintf(stderr, "Failed to bind OpenGL ES API\n"); return false; } EGLint config_attribs[] = { 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_ES2_BIT, EGL_NONE }; EGLConfig config; EGLint num_configs; if (!eglChooseConfig(S.egl_display, config_attribs, &config, 1, &num_configs) || num_configs == 0) { fprintf(stderr, "Failed to choose EGL config\n"); return false; } EGLint context_attribs[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; S.egl_context = eglCreateContext(S.egl_display, config, EGL_NO_CONTEXT, context_attribs); if (S.egl_context == EGL_NO_CONTEXT) { fprintf(stderr, "Failed to create EGL context\n"); return false; } /* Create EGL window surface */ S.egl_window = wl_egl_window_create(S.surface, S.surface_width, S.surface_height); if (!S.egl_window) { fprintf(stderr, "Failed to create wl_egl_window\n"); return false; } S.egl_surface = eglCreateWindowSurface(S.egl_display, config, (EGLNativeWindowType)S.egl_window, NULL); if (S.egl_surface == EGL_NO_SURFACE) { fprintf(stderr, "Failed to create EGL surface\n"); return false; } if (!eglMakeCurrent(S.egl_display, S.egl_surface, S.egl_surface, S.egl_context)) { fprintf(stderr, "eglMakeCurrent failed\n"); return false; } /* Set swap interval to 0 for manual frame pacing */ eglSwapInterval(S.egl_display, 0); fprintf(stderr, "EGL surface created: %dx%d\n", S.surface_width, S.surface_height); return true; } /* ── Shader compilation ───────────────────────────────────────────── */ static const char *vertex_shader_src = "attribute vec2 a_pos;\n" "attribute vec2 a_uv;\n" "attribute vec4 a_color;\n" "varying vec2 v_uv;\n" "varying vec4 v_color;\n" "uniform mat4 u_projection;\n" "void main() {\n" " gl_Position = u_projection * vec4(a_pos, 0.0, 1.0);\n" " v_uv = a_uv;\n" " v_color = a_color;\n" "}\n"; static const char *fragment_shader_src = "precision mediump float;\n" "varying vec2 v_uv;\n" "varying vec4 v_color;\n" "uniform sampler2D u_atlas;\n" "void main() {\n" " float a = texture2D(u_atlas, v_uv).r;\n" " if (a < 0.01) discard;\n" " float scanline = 1.0 - 0.08 * mod(gl_FragCoord.y, 2.0);\n" " gl_FragColor = vec4(v_color.rgb * a * scanline, 1.0);\n" "}\n"; static GLuint render_compile_shader(GLenum type, const char *src) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &src, NULL); glCompileShader(shader); GLint compiled; glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled); if (!compiled) { char log[512]; glGetShaderInfoLog(shader, sizeof(log), NULL, log); fprintf(stderr, "Shader compile error: %s\n", log); glDeleteShader(shader); return 0; } return shader; } static bool render_init_shaders(void) { GLuint vs = render_compile_shader(GL_VERTEX_SHADER, vertex_shader_src); GLuint fs = render_compile_shader(GL_FRAGMENT_SHADER, fragment_shader_src); if (!vs || !fs) return false; S.shader_program = glCreateProgram(); glAttachShader(S.shader_program, vs); glAttachShader(S.shader_program, fs); glLinkProgram(S.shader_program); GLint linked; glGetProgramiv(S.shader_program, GL_LINK_STATUS, &linked); if (!linked) { char log[512]; glGetProgramInfoLog(S.shader_program, sizeof(log), NULL, log); fprintf(stderr, "Shader link error: %s\n", log); return false; } glDeleteShader(vs); glDeleteShader(fs); S.u_projection = glGetUniformLocation(S.shader_program, "u_projection"); S.u_atlas = glGetUniformLocation(S.shader_program, "u_atlas"); S.a_pos = glGetAttribLocation(S.shader_program, "a_pos"); S.a_uv = glGetAttribLocation(S.shader_program, "a_uv"); S.a_color = glGetAttribLocation(S.shader_program, "a_color"); /* Create VBO */ glGenBuffers(1, &S.vbo); /* Allocate CPU-side vertex buffer */ S.vertex_buf = malloc(MAX_QUADS * VERTS_PER_QUAD * sizeof(vertex_t)); S.vertex_count = 0; fprintf(stderr, "Shaders compiled and linked\n"); return true; } static void render_set_projection(int width, int height) { /* Orthographic projection: (0,0) top-left, (w,h) bottom-right */ float proj[16] = { 2.0f / width, 0.0f, 0.0f, 0.0f, 0.0f, -2.0f / height, 0.0f, 0.0f, 0.0f, 0.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, }; glUseProgram(S.shader_program); glUniformMatrix4fv(S.u_projection, 1, GL_FALSE, proj); } /* ── Vertex buffer helpers ────────────────────────────────────────── */ static void render_push_quad(float x0, float y0, float x1, float y1, float u0, float v0, float u1, float v1, float r, float g, float b, float a) { if (S.vertex_count + VERTS_PER_QUAD > MAX_QUADS * VERTS_PER_QUAD) return; vertex_t *v = &S.vertex_buf[S.vertex_count]; /* Triangle 1: top-left, top-right, bottom-left */ v[0] = (vertex_t){ x0, y0, u0, v0, r, g, b, a }; v[1] = (vertex_t){ x1, y0, u1, v0, r, g, b, a }; v[2] = (vertex_t){ x0, y1, u0, v1, r, g, b, a }; /* Triangle 2: top-right, bottom-right, bottom-left */ v[3] = (vertex_t){ x1, y0, u1, v0, r, g, b, a }; v[4] = (vertex_t){ x1, y1, u1, v1, r, g, b, a }; v[5] = (vertex_t){ x0, y1, u0, v1, r, g, b, a }; S.vertex_count += VERTS_PER_QUAD; } static void render_flush_quads(void) { if (S.vertex_count == 0) return; glBindBuffer(GL_ARRAY_BUFFER, S.vbo); glBufferData(GL_ARRAY_BUFFER, S.vertex_count * sizeof(vertex_t), S.vertex_buf, GL_STREAM_DRAW); glEnableVertexAttribArray(S.a_pos); glEnableVertexAttribArray(S.a_uv); glEnableVertexAttribArray(S.a_color); glVertexAttribPointer(S.a_pos, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, x)); glVertexAttribPointer(S.a_uv, 2, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, u)); glVertexAttribPointer(S.a_color, 4, GL_FLOAT, GL_FALSE, sizeof(vertex_t), (void *)offsetof(vertex_t, r)); glDrawArrays(GL_TRIANGLES, 0, S.vertex_count); glDisableVertexAttribArray(S.a_pos); glDisableVertexAttribArray(S.a_uv); glDisableVertexAttribArray(S.a_color); S.vertex_count = 0; } /* ── Matrix engine ────────────────────────────────────────────────── */ static void render_tick_layer(depth_layer_t *layer); static void render_get_layer_properties(int layer_idx, int num_layers, double *font_scale, double *opacity, double *speed_scale, double *density) { if (num_layers == 1) { *font_scale = 1.0; *opacity = 1.0; *speed_scale = 1.0; /* density from config, use midpoint of min/max */ *density = (S.min_density + S.max_density) * 0.5; return; } double t = (double)layer_idx / (num_layers - 1); *font_scale = LAYER_FAR_FONT_SCALE + t * (LAYER_NEAR_FONT_SCALE - LAYER_FAR_FONT_SCALE); *opacity = LAYER_FAR_OPACITY + t * (LAYER_NEAR_OPACITY - LAYER_FAR_OPACITY); *speed_scale = LAYER_FAR_SPEED_SCALE + t * (LAYER_NEAR_SPEED_SCALE - LAYER_FAR_SPEED_SCALE); *density = S.layer_far_density + t * (S.layer_near_density - S.layer_far_density); } static void render_init_layer(depth_layer_t *layer, int width, int height, int layer_idx) { render_get_layer_properties(layer_idx, S.num_depth_layers, &layer->font_scale, &layer->opacity, &layer->speed_scale, &layer->density); /* Cell size: scale the atlas cell size by font_scale */ layer->cell_w = (int)(S.atlas.cell_w * layer->font_scale + 0.5); layer->cell_h = (int)(S.atlas.cell_h * layer->font_scale + 0.5); if (layer->cell_w < 4) layer->cell_w = 4; if (layer->cell_h < 6) layer->cell_h = 6; layer->cols = width / layer->cell_w; layer->rows = height / layer->cell_h; if (layer->cols < 1) layer->cols = 1; if (layer->rows < 1) layer->rows = 1; free(layer->columns); free(layer->cells); layer->columns = calloc(layer->cols, sizeof(render_column_t)); layer->cells = calloc(layer->rows * layer->cols, sizeof(render_cell_t)); double min_spd = S.min_speed * layer->speed_scale; double max_spd = S.max_speed * layer->speed_scale; double avg_spd = S.avg_speed * layer->speed_scale; for (int c = 0; c < layer->cols; c++) { render_column_t *col = &layer->columns[c]; col->x_offset = (render_randf() - 0.5f) * layer->cell_w * 0.5f; col->skip = (render_randf() < 0.15); if (render_randf() < layer->density) { col->active = true; col->color = render_trail_color(); col->speed = render_rand_speed(min_spd, max_spd, avg_spd); /* Spread start positions across the full screen height so columns appear mid-fall from the start */ col->head_pos = render_randf() * layer->rows * 2 - layer->rows; col->trail_len = render_randi(S.min_trail, S.max_trail); } else { col->active = false; col->respawn_timer = render_randi(1, RESPAWN_DELAY_MAX); } } /* Pre-populate cells for columns that are already mid-screen */ for (int i = 0; i < layer->rows * layer->cols; i++) { layer->cells[i].codepoint = render_random_codepoint(); layer->cells[i].age = 9999; layer->cells[i].mirrored = (render_randf() < 0.20); } for (int c = 0; c < layer->cols; c++) { render_column_t *col = &layer->columns[c]; if (!col->active || col->skip) continue; int head = (int)col->head_pos; /* Fill trail behind the head with aged characters */ for (int t = 0; t < col->trail_len && head - t >= 0; t++) { int r = head - t; if (r < layer->rows) { render_cell_t *cell = render_cell_at_layer(layer, r, c); cell->codepoint = render_random_codepoint(); cell->age = t; cell->mirrored = (render_randf() < 0.20); } } } } static void render_init_matrix(int width, int height) { S.surface_width = width; S.surface_height = height; for (int l = 0; l < S.num_depth_layers; l++) { render_init_layer(&S.layers[l], width, height, l); } /* Pre-run the simulation so first frame looks like it's been running for a while — no visible startup sweep */ int warmup = 60 + rand() % 120; /* 2-6 seconds at 30fps */ for (int f = 0; f < warmup; f++) { for (int l = 0; l < S.num_depth_layers; l++) { render_tick_layer(&S.layers[l]); } } } static void render_tick_layer(depth_layer_t *layer) { double min_spd = S.min_speed * layer->speed_scale; double max_spd = S.max_speed * layer->speed_scale; double avg_spd = S.avg_speed * layer->speed_scale; for (int c = 0; c < layer->cols; c++) { render_column_t *col = &layer->columns[c]; if (col->skip) continue; if (!col->active) { if (--col->respawn_timer <= 0) { col->active = true; col->color = render_trail_color(); col->speed = render_rand_speed(min_spd, max_spd, avg_spd); col->head_pos = -1.0; col->trail_len = render_randi(S.min_trail, S.max_trail); } continue; } col->head_pos += col->speed; int head_row = (int)col->head_pos; if (head_row >= 0 && head_row < layer->rows) { render_cell_t *cell = render_cell_at_layer(layer, head_row, c); cell->codepoint = render_random_codepoint(); cell->age = 0; cell->mirrored = (render_randf() < 0.20); } /* Flash columns: ~0.3% chance per frame of starting a flash */ if (col->flash_timer > 0) { col->flash_timer--; } else if (render_randf() < 0.003) { col->flash_timer = 6 + rand() % 5; /* 6-10 frames */ col->flash_brightness = 1.8f; } for (int r = 0; r < layer->rows; r++) { render_cell_t *cell = render_cell_at_layer(layer, r, c); if (cell->age < 9999) { cell->age++; } if (cell->age > 0 && cell->age < col->trail_len && render_randf() < MUTATION_CHANCE) { cell->codepoint = render_random_codepoint(); } } if (head_row > layer->rows + col->trail_len) { col->active = false; col->respawn_timer = render_randi(1, RESPAWN_DELAY_MAX); for (int r = 0; r < layer->rows; r++) { render_cell_at_layer(layer, r, c)->age = 9999; } } } } static void render_tick_matrix(void) { for (int l = 0; l < S.num_depth_layers; l++) { render_tick_layer(&S.layers[l]); } } /* ── Rendering ────────────────────────────────────────────────────── */ static void render_frame(void) { int w = S.surface_width; int h = S.surface_height; glViewport(0, 0, w, h); /* glClearColor/glClear is handled by the phase logic in the main loop */ glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glUseProgram(S.shader_program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, S.atlas.texture); glUniform1i(S.u_atlas, 0); float fade = S.fade_opacity; /* Render layers back-to-front (0=far, N-1=near) */ for (int l = 0; l < S.num_depth_layers; l++) { depth_layer_t *layer = &S.layers[l]; float alpha = (float)layer->opacity * fade; float scale = (float)layer->font_scale; S.vertex_count = 0; for (int c = 0; c < layer->cols; c++) { render_column_t *col = &layer->columns[c]; if (col->skip) continue; for (int r = 0; r < layer->rows; r++) { render_cell_t *cell = render_cell_at_layer(layer, r, c); if (cell->age >= 9999) continue; float brightness; bool is_head = false; if (cell->age == 0 && col->active) { is_head = true; brightness = 1.0f; } else if (cell->age < col->trail_len) { float t = 1.0f - (float)cell->age / col->trail_len; brightness = t * t; if (brightness < 0.05f) continue; } else { continue; } /* Flash column boost */ if (col->flash_timer > 0) { float flash_t = (float)col->flash_timer / 10.0f; float boost = 1.0f + (col->flash_brightness - 1.0f) * flash_t; brightness *= boost; if (brightness > 1.0f && !is_head) brightness = 1.0f; } const glyph_info_t *gi = render_find_glyph(cell->codepoint); if (!gi) continue; /* Position the glyph within the cell */ float cell_x = (float)(c * layer->cell_w) + layer->columns[c].x_offset; float cell_y = (float)(r * layer->cell_h); /* Scale glyph dimensions by layer font_scale relative to atlas */ float gw = gi->width * scale; float gh = gi->height * scale; float bx = gi->bearing_x * scale; float by = gi->bearing_y * scale; /* Center glyph in cell: x based on bearing, y based on bearing */ float x0 = cell_x + bx; float y0 = cell_y + (layer->cell_h - by); float x1 = x0 + gw; float y1 = y0 + gh; /* UV coordinates — possibly mirrored horizontally */ float qu0 = cell->mirrored ? gi->u1 : gi->u0; float qu1 = cell->mirrored ? gi->u0 : gi->u1; /* Determine color: white-to-trail gradient for head and near-head */ rgb_t tc = col->color; float cr, cg, cb; if (is_head) { /* age==0: pure white */ cr = 1.0f; cg = 1.0f; cb = 1.0f; } else if (cell->age == 1) { /* 70% white + 30% trail color */ cr = 0.7f + 0.3f * tc.r; cg = 0.7f + 0.3f * tc.g; cb = 0.7f + 0.3f * tc.b; } else if (cell->age == 2) { /* 40% white + 60% trail color */ cr = 0.4f + 0.6f * tc.r; cg = 0.4f + 0.6f * tc.g; cb = 0.4f + 0.6f * tc.b; } else { /* pure trail color */ cr = tc.r; cg = tc.g; cb = tc.b; } if (is_head) { /* Enhanced "type-in" glow: 50% larger pad, stronger alpha */ float glow_pad = HEAD_GLOW_EXTRA * scale * 1.5f; if (glow_pad < 1.5f) glow_pad = 1.5f; float ga = 0.6f * alpha; render_push_quad(x0 - glow_pad, y0 - glow_pad, x1 + glow_pad, y1 + glow_pad, qu0, gi->v0, qu1, gi->v1, cr * ga, cg * ga, cb * ga, 1.0f); /* Head character: full brightness white */ render_push_quad(x0, y0, x1, y1, qu0, gi->v0, qu1, gi->v1, cr * alpha, cg * alpha, cb * alpha, 1.0f); } else { float b = brightness * alpha; render_push_quad(x0, y0, x1, y1, qu0, gi->v0, qu1, gi->v1, cr * b, cg * b, cb * b, 1.0f); } } } render_flush_quads(); } } /* ── Wayland output tracking ──────────────────────────────────────── */ struct render_output_info { struct wl_output *output; struct zxdg_output_v1 *xdg_output; char name[128]; int width, height; int scale; struct wl_list link; }; static struct wl_list render_output_list; static void render_xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { (void)xdg_output; struct render_output_info *info = data; snprintf(info->name, sizeof(info->name), "%s", name); if (strcmp(name, S.target_name) == 0) { S.target_output = info->output; S.output_found = true; S.output_width = info->width; S.output_height = info->height; S.output_scale = info->scale > 0 ? info->scale : 1; } } static void render_xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { (void)data; (void)xdg_output; (void)x; (void)y; } static void render_xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t w, int32_t h) { (void)xdg_output; struct render_output_info *info = data; info->width = w; info->height = h; if (strcmp(info->name, S.target_name) == 0) { S.output_width = w; S.output_height = h; } } static void render_xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { (void)data; (void)xdg_output; } static void render_xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *desc) { (void)data; (void)xdg_output; (void)desc; } static const struct zxdg_output_v1_listener render_xdg_output_listener = { .logical_position = render_xdg_output_handle_logical_position, .logical_size = render_xdg_output_handle_logical_size, .done = render_xdg_output_handle_done, .name = render_xdg_output_handle_name, .description = render_xdg_output_handle_description, }; static void render_output_handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t pw, int32_t ph, int32_t subpixel, const char *make, const char *model, int32_t transform) { (void)data; (void)output; (void)x; (void)y; (void)pw; (void)ph; (void)subpixel; (void)make; (void)model; (void)transform; } static void render_output_handle_mode(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { (void)output; (void)refresh; struct render_output_info *info = data; if (flags & WL_OUTPUT_MODE_CURRENT) { info->width = width; info->height = height; } } static void render_output_handle_scale(void *data, struct wl_output *output, int32_t factor) { (void)output; struct render_output_info *info = data; info->scale = factor; } static void render_output_handle_done(void *data, struct wl_output *output) { (void)data; (void)output; } static void render_output_handle_name(void *data, struct wl_output *output, const char *name) { (void)output; struct render_output_info *info = data; snprintf(info->name, sizeof(info->name), "%s", name); if (strcmp(name, S.target_name) == 0) { S.target_output = info->output; S.output_found = true; S.output_width = info->width; S.output_height = info->height; S.output_scale = info->scale > 0 ? info->scale : 1; } } static void render_output_handle_description(void *data, struct wl_output *output, const char *desc) { (void)data; (void)output; (void)desc; } static const struct wl_output_listener render_output_listener = { .geometry = render_output_handle_geometry, .mode = render_output_handle_mode, .done = render_output_handle_done, .scale = render_output_handle_scale, .name = render_output_handle_name, .description = render_output_handle_description, }; /* ── Layer surface events ─────────────────────────────────────────── */ static void render_layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t width, uint32_t height) { (void)data; S.configure_serial = serial; S.surface_width = width ? (int)width : S.output_width; S.surface_height = height ? (int)height : S.output_height; S.configured = true; zwlr_layer_surface_v1_ack_configure(surface, serial); } static void render_layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) { (void)data; (void)surface; S.closed = true; } static const struct zwlr_layer_surface_v1_listener render_layer_surface_listener = { .configure = render_layer_surface_configure, .closed = render_layer_surface_closed, }; /* ── Registry ─────────────────────────────────────────────────────── */ static void render_registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { (void)data; if (strcmp(interface, wl_compositor_interface.name) == 0) { S.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4); } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { S.layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, version < 3 ? version : 3); } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { S.xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, version < 3 ? version : 3); } else if (strcmp(interface, wl_output_interface.name) == 0) { struct render_output_info *info = calloc(1, sizeof(*info)); info->output = wl_registry_bind(registry, name, &wl_output_interface, version < 4 ? version : 4); info->scale = 1; wl_output_add_listener(info->output, &render_output_listener, info); wl_list_insert(&render_output_list, &info->link); } } static void render_registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data; (void)registry; (void)name; } static const struct wl_registry_listener render_registry_listener = { .global = render_registry_handle_global, .global_remove = render_registry_handle_global_remove, }; /* ── Renderer signal handling ─────────────────────────────────────── */ static void render_sig_handler(int sig) { (void)sig; S.stop = 1; } static void render_sigusr1_handler(int sig) { (void)sig; S.fade_out_requested = 1; } static void ensure_wayland_display(void) { const char *display = getenv("WAYLAND_DISPLAY"); const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); if ((display && display[0] != '\0') || !runtime_dir || runtime_dir[0] == '\0') return; DIR *dir = opendir(runtime_dir); if (!dir) return; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { if (strncmp(ent->d_name, "wayland-", 8) != 0) continue; char path[512]; struct stat st; snprintf(path, sizeof(path), "%s/%s", runtime_dir, ent->d_name); if (stat(path, &st) == 0 && S_ISSOCK(st.st_mode)) { setenv("WAYLAND_DISPLAY", ent->d_name, 0); break; } } closedir(dir); } /* ── Renderer main ────────────────────────────────────────────────── */ static int renderer_main(int argc, char **argv) { /* Defaults */ S.target_name[0] = '\0'; S.font_size = 14; S.base_color = (rgb_t){ 0.0f, 1.0f, 0.0f }; S.output_scale = 1; S.min_speed = DEFAULT_MIN_SPEED; S.max_speed = DEFAULT_MAX_SPEED; S.avg_speed = DEFAULT_AVG_SPEED; S.min_trail = DEFAULT_MIN_TRAIL; S.max_trail = DEFAULT_MAX_TRAIL; S.min_density = DEFAULT_MIN_DENSITY; S.max_density = DEFAULT_MAX_DENSITY; S.layer_near_density = LAYER_NEAR_DENSITY; S.layer_far_density = LAYER_FAR_DENSITY; S.num_depth_layers = DEFAULT_DEPTH_LAYERS; S.fade_frames = DEFAULT_FADE_FRAMES; const char *color_name = "green"; /* Parse args (skip --render which was already consumed) */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--render") == 0) { continue; /* already handled */ } else if (strcmp(argv[i], "--output") == 0 && i + 1 < argc) { snprintf(S.target_name, sizeof(S.target_name), "%s", argv[++i]); } else if (strcmp(argv[i], "--color") == 0 && i + 1 < argc) { color_name = argv[++i]; } else if (strcmp(argv[i], "--font-size") == 0 && i + 1 < argc) { S.font_size = atoi(argv[++i]); if (S.font_size < 6) S.font_size = 6; if (S.font_size > 72) S.font_size = 72; } else if (strcmp(argv[i], "--min-speed") == 0 && i + 1 < argc) { S.min_speed = atof(argv[++i]); } else if (strcmp(argv[i], "--max-speed") == 0 && i + 1 < argc) { S.max_speed = atof(argv[++i]); } else if (strcmp(argv[i], "--avg-speed") == 0 && i + 1 < argc) { S.avg_speed = atof(argv[++i]); } else if (strcmp(argv[i], "--min-trail") == 0 && i + 1 < argc) { S.min_trail = atoi(argv[++i]); } else if (strcmp(argv[i], "--max-trail") == 0 && i + 1 < argc) { S.max_trail = atoi(argv[++i]); } else if (strcmp(argv[i], "--min-density") == 0 && i + 1 < argc) { S.min_density = atof(argv[++i]); } else if (strcmp(argv[i], "--max-density") == 0 && i + 1 < argc) { S.max_density = atof(argv[++i]); } else if (strcmp(argv[i], "--speed") == 0 && i + 1 < argc) { double mult = atof(argv[++i]); if (mult < 0.1) mult = 0.1; if (mult > 5.0) mult = 5.0; S.min_speed *= mult; S.max_speed *= mult; } else if (strcmp(argv[i], "--depth-layers") == 0 && i + 1 < argc) { S.num_depth_layers = atoi(argv[++i]); if (S.num_depth_layers < MIN_DEPTH_LAYERS) S.num_depth_layers = MIN_DEPTH_LAYERS; if (S.num_depth_layers > MAX_DEPTH_LAYERS) S.num_depth_layers = MAX_DEPTH_LAYERS; } else if (strcmp(argv[i], "--layer-near-density") == 0 && i + 1 < argc) { S.layer_near_density = atof(argv[++i]); } else if (strcmp(argv[i], "--layer-far-density") == 0 && i + 1 < argc) { S.layer_far_density = atof(argv[++i]); } else if (strcmp(argv[i], "--fade-frames") == 0 && i + 1 < argc) { S.fade_frames = atoi(argv[++i]); if (S.fade_frames < 1) S.fade_frames = 1; if (S.fade_frames > 300) S.fade_frames = 300; } else if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { fprintf(stderr, "Usage: %s --render --output [--color ] " "[--font-size ]\n" " [--min-speed ] [--max-speed ] " "[--min-trail ] [--max-trail ]\n" " [--min-density ] [--max-density ]\n" " [--speed ] [--depth-layers ]\n" "\n" "Colors: green, red, blue, white, yellow, cyan, magenta, random\n" "--speed scales both min/max speed (e.g. --speed 2.0 = double speed)\n" "--depth-layers: number of parallax layers (1-%d, default %d)\n" "Default font-size: 14\n" "Speed range: %.1f - %.1f Trail range: %d - %d " "Density range: %.2f - %.2f\n", argv[0], MAX_DEPTH_LAYERS, DEFAULT_DEPTH_LAYERS, DEFAULT_MIN_SPEED, DEFAULT_MAX_SPEED, DEFAULT_MIN_TRAIL, DEFAULT_MAX_TRAIL, DEFAULT_MIN_DENSITY, DEFAULT_MAX_DENSITY); return 0; } else { fprintf(stderr, "Unknown option: %s\n", argv[i]); return 1; } } /* Clamp values */ if (S.min_speed < 0.1) S.min_speed = 0.1; if (S.max_speed < S.min_speed) S.max_speed = S.min_speed; if (S.min_trail < 1) S.min_trail = 1; if (S.max_trail < S.min_trail) S.max_trail = S.min_trail; if (S.min_density < 0.01) S.min_density = 0.01; if (S.min_density > 1.0) S.min_density = 1.0; if (S.max_density < S.min_density) S.max_density = S.min_density; if (S.max_density > 1.0) S.max_density = 1.0; if (S.target_name[0] == '\0') { fprintf(stderr, "Error: --output is required in render mode\n"); return 1; } srand((unsigned)time(NULL) ^ (unsigned)getpid()); S.base_color = render_lookup_color(color_name); if (strcasecmp(color_name, "random_trail_named") == 0) S.color_mode = COLOR_RANDOM; else if (strcasecmp(color_name, "random_trail_hex") == 0) S.color_mode = COLOR_RANDOM_HEX; else S.color_mode = COLOR_FIXED; /* Signal handlers */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = render_sig_handler; sigemptyset(&sa.sa_mask); sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); struct sigaction sa_usr1; memset(&sa_usr1, 0, sizeof(sa_usr1)); sa_usr1.sa_handler = render_sigusr1_handler; sigemptyset(&sa_usr1.sa_mask); sigaction(SIGUSR1, &sa_usr1, NULL); signal(SIGPIPE, SIG_IGN); /* Connect to Wayland */ ensure_wayland_display(); S.display = wl_display_connect(NULL); if (!S.display) { fprintf(stderr, "Cannot connect to Wayland display\n"); return 1; } wl_list_init(&render_output_list); S.registry = wl_display_get_registry(S.display); wl_registry_add_listener(S.registry, &render_registry_listener, NULL); wl_display_roundtrip(S.display); if (S.xdg_output_manager) { struct render_output_info *info; wl_list_for_each(info, &render_output_list, link) { info->xdg_output = zxdg_output_manager_v1_get_xdg_output( S.xdg_output_manager, info->output); zxdg_output_v1_add_listener(info->xdg_output, &render_xdg_output_listener, info); } } wl_display_roundtrip(S.display); if (!S.output_found || !S.target_output) { fprintf(stderr, "Output '%s' not found. Available outputs:\n", S.target_name); struct render_output_info *info; wl_list_for_each(info, &render_output_list, link) { fprintf(stderr, " %s (%dx%d)\n", info->name, info->width, info->height); } wl_display_disconnect(S.display); return 1; } if (!S.compositor || !S.layer_shell) { fprintf(stderr, "Missing required Wayland interfaces " "(compositor=%p layer_shell=%p)\n", (void*)S.compositor, (void*)S.layer_shell); wl_display_disconnect(S.display); return 1; } fprintf(stderr, "Target output: %s (%dx%d, scale=%d, depth_layers=%d, speed=%.1f-%.1f, trail=%d-%d, density=%.2f-%.2f)\n", S.target_name, S.output_width, S.output_height, S.output_scale, S.num_depth_layers, S.min_speed, S.max_speed, S.min_trail, S.max_trail, S.min_density, S.max_density); /* Create surface */ S.surface = wl_compositor_create_surface(S.compositor); S.layer_surface = zwlr_layer_shell_v1_get_layer_surface( S.layer_shell, S.surface, S.target_output, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "oledsaver"); zwlr_layer_surface_v1_set_anchor(S.layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); zwlr_layer_surface_v1_set_size(S.layer_surface, 0, 0); zwlr_layer_surface_v1_set_exclusive_zone(S.layer_surface, -1); zwlr_layer_surface_v1_set_keyboard_interactivity(S.layer_surface, 0); /* Keep the visual overlay from receiving pointer/touch input. */ struct wl_region *empty_input = wl_compositor_create_region(S.compositor); wl_surface_set_input_region(S.surface, empty_input); wl_region_destroy(empty_input); zwlr_layer_surface_v1_add_listener(S.layer_surface, &render_layer_surface_listener, NULL); wl_surface_commit(S.surface); wl_display_roundtrip(S.display); if (!S.configured) { fprintf(stderr, "Layer surface was not configured\n"); wl_display_disconnect(S.display); return 1; } fprintf(stderr, "Surface configured: %dx%d\n", S.surface_width, S.surface_height); /* Initialize EGL */ if (!render_init_egl()) { fprintf(stderr, "Failed to initialize EGL\n"); wl_display_disconnect(S.display); return 1; } /* Initialize shaders */ if (!render_init_shaders()) { fprintf(stderr, "Failed to initialize shaders\n"); wl_display_disconnect(S.display); return 1; } render_set_projection(S.surface_width, S.surface_height); /* Initialize FreeType and build glyph atlas */ int atlas_font_size = (int)(S.font_size * LAYER_NEAR_FONT_SCALE + 0.5); if (atlas_font_size < S.font_size) atlas_font_size = S.font_size; if (!render_init_freetype(atlas_font_size)) { fprintf(stderr, "Failed to initialize FreeType\n"); wl_display_disconnect(S.display); return 1; } if (!render_build_atlas(atlas_font_size)) { fprintf(stderr, "Failed to build glyph atlas\n"); wl_display_disconnect(S.display); return 1; } fprintf(stderr, "Initializing %d depth layers (font_size=%d, atlas_size=%d)\n", S.num_depth_layers, S.font_size, atlas_font_size); render_init_matrix(S.surface_width, S.surface_height); for (int l = 0; l < S.num_depth_layers; l++) { depth_layer_t *dl = &S.layers[l]; fprintf(stderr, "Layer %d: font_scale=%.2f cell=%dx%d grid=%dx%d opacity=%.2f speed=%.2fx density=%.0f%%\n", l, dl->font_scale, dl->cell_w, dl->cell_h, dl->cols, dl->rows, dl->opacity, dl->speed_scale, dl->density * 100.0); } fprintf(stderr, "Starting matrix rain with depth layers (OpenGL ES 2.0)\n"); /* Initialize phase state (warmup already ran, now fade desktop to black) */ S.phase = PHASE_FADE_TO_BLACK; S.phase_frame = 0; S.surface_alpha = 0.0f; S.fade_opacity = 0.0f; /* Main render loop */ struct timespec ts_start, ts_end; while (!S.stop && !S.closed) { clock_gettime(CLOCK_MONOTONIC, &ts_start); if (wl_display_prepare_read(S.display) == 0) { wl_display_flush(S.display); struct pollfd pfd = { .fd = wl_display_get_fd(S.display), .events = POLLIN, }; if (poll(&pfd, 1, 0) > 0) { wl_display_read_events(S.display); } else { wl_display_cancel_read(S.display); } } wl_display_dispatch_pending(S.display); if (S.stop || S.closed) break; glViewport(0, 0, S.surface_width, S.surface_height); switch (S.phase) { case PHASE_FADE_TO_BLACK: S.phase_frame++; S.surface_alpha = (float)S.phase_frame / S.fade_frames; if (S.surface_alpha >= 1.0f) { S.surface_alpha = 1.0f; S.phase = PHASE_FADE_IN; S.phase_frame = 0; } /* Clear with transitioning alpha, don't render rain */ glClearColor(0.0f, 0.0f, 0.0f, S.surface_alpha); glClear(GL_COLOR_BUFFER_BIT); break; case PHASE_FADE_IN: S.phase_frame++; S.fade_opacity = (float)S.phase_frame / S.fade_frames; if (S.fade_opacity >= 1.0f) { S.fade_opacity = 1.0f; S.phase = PHASE_RUNNING; } glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); render_tick_matrix(); render_frame(); break; case PHASE_RUNNING: if (S.fade_out_requested) { S.phase = PHASE_FADE_OUT; S.phase_frame = S.fade_frames; S.fade_out_requested = 0; } glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); render_tick_matrix(); render_frame(); break; case PHASE_FADE_OUT: S.phase_frame--; S.fade_opacity = (float)S.phase_frame / S.fade_frames; if (S.fade_opacity <= 0.0f) { S.fade_opacity = 0.0f; S.phase = PHASE_FADE_TO_DESKTOP; S.phase_frame = S.fade_frames; } glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); render_tick_matrix(); render_frame(); break; case PHASE_FADE_TO_DESKTOP: S.phase_frame--; S.surface_alpha = (float)S.phase_frame / S.fade_frames; if (S.surface_alpha <= 0.0f) { S.surface_alpha = 0.0f; S.stop = 1; /* exit */ } glClearColor(0.0f, 0.0f, 0.0f, S.surface_alpha); glClear(GL_COLOR_BUFFER_BIT); /* Don't render rain — just fading black overlay */ break; } eglSwapBuffers(S.egl_display, S.egl_surface); clock_gettime(CLOCK_MONOTONIC, &ts_end); long elapsed_us = (ts_end.tv_sec - ts_start.tv_sec) * 1000000L + (ts_end.tv_nsec - ts_start.tv_nsec) / 1000; long sleep_us = FRAME_INTERVAL_US - elapsed_us; if (sleep_us > 0) { usleep((useconds_t)sleep_us); } } fprintf(stderr, "Shutting down\n"); /* Cleanup */ free(S.vertex_buf); if (S.vbo) glDeleteBuffers(1, &S.vbo); if (S.shader_program) glDeleteProgram(S.shader_program); if (S.atlas.texture) glDeleteTextures(1, &S.atlas.texture); if (S.egl_display != EGL_NO_DISPLAY) { eglMakeCurrent(S.egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (S.egl_surface != EGL_NO_SURFACE) eglDestroySurface(S.egl_display, S.egl_surface); if (S.egl_context != EGL_NO_CONTEXT) eglDestroyContext(S.egl_display, S.egl_context); eglTerminate(S.egl_display); } if (S.egl_window) wl_egl_window_destroy(S.egl_window); if (S.ft_face) FT_Done_Face(S.ft_face); if (S.ft_library) FT_Done_FreeType(S.ft_library); if (S.layer_surface) zwlr_layer_surface_v1_destroy(S.layer_surface); if (S.surface) wl_surface_destroy(S.surface); for (int l = 0; l < S.num_depth_layers; l++) { free(S.layers[l].columns); free(S.layers[l].cells); } struct render_output_info *info, *tmp; wl_list_for_each_safe(info, tmp, &render_output_list, link) { if (info->xdg_output) zxdg_output_v1_destroy(info->xdg_output); wl_output_release(info->output); wl_list_remove(&info->link); free(info); } if (S.xdg_output_manager) zxdg_output_manager_v1_destroy(S.xdg_output_manager); if (S.registry) wl_registry_destroy(S.registry); wl_display_disconnect(S.display); return 0; } /* =================================================================== * MANAGER MODE (Wayfire IPC idle blanking) * =================================================================== */ /* ── Constants ─────────────────────────────────────────────────────── */ #define MAX_OUTPUTS 8 #define MAX_OUTPUT_NAME 64 #define MAX_PATH_LEN 512 #define MAX_CMD_LEN 1024 #define MAX_PIDS 4 #define WF_IPC_HEADER_LEN 4 #define WF_IPC_MAX_PAYLOAD (16 * 1024 * 1024) #define CURSOR_POLL_INTERVAL 0.25 static const char *MGR_COLORS[] = { "green", "red", "blue", "white", "yellow", "cyan", "magenta" }; #define MGR_NUM_COLORS 7 /* ── Data structures ───────────────────────────────────────────────── */ typedef struct { char name[MAX_OUTPUT_NAME]; double last_input; char state[8]; pid_t pids[MAX_PIDS]; int pid_count; int timeout_override; char color[32]; /* per-output overrides; negative = use global */ double min_speed, max_speed; int min_trail, max_trail; double min_density, max_density; } mgr_output_info_t; typedef struct { /* Config values */ int global_timeout; int check_interval; int focused_timeout; char log_file[MAX_PATH_LEN]; char matrix_cmd[MAX_CMD_LEN]; char matrix_color[32]; /* Global matrix tuning ranges */ double matrix_min_speed, matrix_max_speed, matrix_avg_speed; int matrix_min_trail, matrix_max_trail; double matrix_min_density, matrix_max_density; double matrix_layer_near_density, matrix_layer_far_density; int matrix_font_size; int matrix_depth_layers; int matrix_fade_frames; char config_file[MAX_PATH_LEN]; double config_mtime; char lock_cmd[MAX_CMD_LEN]; /* command to run for screen lock */ /* State */ mgr_output_info_t outputs[MAX_OUTPUTS]; int output_count; char focused_output[MAX_OUTPUT_NAME]; double last_blank_time; int seat_idle; int wayfire_cursor_hidden; int wayfire_cursor_warned; volatile sig_atomic_t stop; volatile sig_atomic_t activate_all; /* SIGUSR1: blank all outputs now */ volatile sig_atomic_t lock_now; /* SIGUSR2: blank all + run lock cmd */ /* Sync */ pthread_mutex_t lock; /* Logging */ FILE *log_fp; } mgr_state_t; static mgr_state_t G; /* ── Logging ───────────────────────────────────────────────────────── */ static void mgr_log_msg(const char *level, const char *fmt, ...) { time_t now = time(NULL); struct tm tm; char tbuf[64]; localtime_r(&now, &tm); strftime(tbuf, sizeof(tbuf), "%Y-%m-%d %H:%M:%S", &tm); va_list ap; va_start(ap, fmt); fprintf(stdout, "%s %s ", tbuf, level); vfprintf(stdout, fmt, ap); fputc('\n', stdout); fflush(stdout); va_end(ap); if (G.log_fp) { va_start(ap, fmt); fprintf(G.log_fp, "%s %s ", tbuf, level); vfprintf(G.log_fp, fmt, ap); fputc('\n', G.log_fp); fflush(G.log_fp); va_end(ap); } } #define LOG_INFO(...) mgr_log_msg("INFO", __VA_ARGS__) #define LOG_WARNING(...) mgr_log_msg("WARNING", __VA_ARGS__) #define LOG_ERROR(...) mgr_log_msg("ERROR", __VA_ARGS__) /* ── Utility ───────────────────────────────────────────────────────── */ static double mgr_monotime(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec + ts.tv_nsec / 1e9; } static int mgr_pid_alive(pid_t pid) { if (pid <= 0) return 0; return kill(pid, 0) == 0; } static void mgr_mkdirs(const char *path) { char tmp[MAX_PATH_LEN]; snprintf(tmp, sizeof(tmp), "%s", path); for (char *p = tmp + 1; *p; p++) { if (*p == '/') { *p = '\0'; mkdir(tmp, 0755); *p = '/'; } } mkdir(tmp, 0755); } static void mgr_runtime_path(char *dst, size_t dstsz, const char *name) { const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); if (runtime_dir && runtime_dir[0] != '\0') { char dir[MAX_PATH_LEN]; snprintf(dir, sizeof(dir), "%s/oledsaver", runtime_dir); mgr_mkdirs(dir); snprintf(dst, dstsz, "%s/%s", dir, name); return; } snprintf(dst, dstsz, "/tmp/%s", name); } static void mgr_expand_tilde(const char *src, char *dst, size_t dstsz) { if (src[0] == '~' && (src[1] == '/' || src[1] == '\0')) { const char *home = getenv("HOME"); if (home) snprintf(dst, dstsz, "%s%s", home, src + 1); else snprintf(dst, dstsz, "%s", src); } else { snprintf(dst, dstsz, "%s", src); } } static const char *mgr_random_color(void) { return MGR_COLORS[rand() % MGR_NUM_COLORS]; } /* ── Wayfire IPC (direct socket) ──────────────────────────────────── */ static int mgr_wayfire_ipc_path(char *dst, size_t dstsz) { const char *sock = getenv("WAYFIRE_SOCKET"); if (!sock || sock[0] == '\0') sock = getenv("_WAYFIRE_SOCKET"); if (sock && sock[0] != '\0') { snprintf(dst, dstsz, "%s", sock); return 0; } ensure_wayland_display(); const char *runtime_dir = getenv("XDG_RUNTIME_DIR"); const char *display = getenv("WAYLAND_DISPLAY"); if (runtime_dir && runtime_dir[0] != '\0' && display && display[0] != '\0') { snprintf(dst, dstsz, "%s/wayfire-%s-.socket", runtime_dir, display); struct stat st; if (stat(dst, &st) == 0 && S_ISSOCK(st.st_mode)) return 0; } if (!runtime_dir || runtime_dir[0] == '\0') return -1; DIR *dir = opendir(runtime_dir); if (!dir) return -1; struct dirent *ent; while ((ent = readdir(dir)) != NULL) { const char *name = ent->d_name; size_t len = strlen(name); if (strncmp(name, "wayfire-", 8) != 0 || len < 15 || strcmp(name + len - 7, ".socket") != 0) continue; char candidate[MAX_PATH_LEN]; struct stat st; snprintf(candidate, sizeof(candidate), "%s/%s", runtime_dir, name); if (stat(candidate, &st) == 0 && S_ISSOCK(st.st_mode)) { snprintf(dst, dstsz, "%s", candidate); closedir(dir); return 0; } } closedir(dir); return -1; } static int mgr_ipc_connect(void) { char sock[MAX_PATH_LEN]; if (mgr_wayfire_ipc_path(sock, sizeof(sock)) < 0) { LOG_ERROR("Wayfire IPC socket not found"); return -1; } if (strlen(sock) >= sizeof(((struct sockaddr_un *)0)->sun_path)) { LOG_ERROR("Wayfire IPC socket path too long: %s", sock); return -1; } int fd = socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) return -1; struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", sock); if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { close(fd); return -1; } return fd; } static int mgr_readn(int fd, void *buf, size_t n) { size_t total = 0; while (total < n) { ssize_t r = read(fd, (char *)buf + total, n - total); if (r < 0 && errno == EINTR) continue; if (r <= 0) return -1; total += r; } return 0; } static int mgr_writen(int fd, const void *buf, size_t n) { size_t total = 0; while (total < n) { ssize_t w = write(fd, (const char *)buf + total, n - total); if (w < 0 && errno == EINTR) continue; if (w <= 0) return -1; total += w; } return 0; } static int mgr_ipc_send_payload(int fd, const char *payload) { uint32_t len = payload ? (uint32_t)strlen(payload) : 0; if (len > WF_IPC_MAX_PAYLOAD) return -1; if (mgr_writen(fd, &len, WF_IPC_HEADER_LEN) < 0) return -1; if (len > 0 && mgr_writen(fd, payload, len) < 0) return -1; return 0; } static int mgr_ipc_send_method(int fd, const char *method, const char *data_json) { struct json_object *root = json_object_new_object(); struct json_object *data = NULL; if (!root) return -1; if (data_json && data_json[0] != '\0') data = json_tokener_parse(data_json); if (!data) data = json_object_new_object(); json_object_object_add(root, "method", json_object_new_string(method)); json_object_object_add(root, "data", data); const char *payload = json_object_to_json_string_ext( root, JSON_C_TO_STRING_PLAIN); int rc = mgr_ipc_send_payload(fd, payload); json_object_put(root); return rc; } static int mgr_ipc_recv(int fd, char **out_payload) { uint32_t len; if (mgr_readn(fd, &len, WF_IPC_HEADER_LEN) < 0) return -1; if (len > WF_IPC_MAX_PAYLOAD) return -1; char *payload = malloc(len + 1); if (!payload) return -1; if (len > 0 && mgr_readn(fd, payload, len) < 0) { free(payload); return -1; } payload[len] = '\0'; if (out_payload) *out_payload = payload; else free(payload); return 0; } static struct json_object *mgr_ipc_request(const char *method, const char *data_json) { int fd = mgr_ipc_connect(); if (fd < 0) return NULL; if (mgr_ipc_send_method(fd, method, data_json) < 0) { close(fd); return NULL; } char *resp = NULL; if (mgr_ipc_recv(fd, &resp) < 0) { close(fd); return NULL; } close(fd); struct json_object *j = json_tokener_parse(resp); free(resp); return j; } /* ── Idle inhibitor detection ──────────────────────────────────────── */ static int mgr_check_idle_inhibitors(void) { return 0; } /* ── Config parsing ────────────────────────────────────────────────── */ static void mgr_trim(char *s) { char *start = s; while (*start == ' ' || *start == '\t') start++; if (start != s) memmove(s, start, strlen(start) + 1); char *end = s + strlen(s) - 1; while (end >= s && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) *end-- = '\0'; } static void mgr_set_defaults(void) { const char *state_home = getenv("XDG_STATE_HOME"); const char *home = getenv("HOME"); G.global_timeout = 300; G.check_interval = 5; G.focused_timeout = 30; G.matrix_cmd[0] = '\0'; G.matrix_min_speed = DEFAULT_MIN_SPEED; G.matrix_max_speed = DEFAULT_MAX_SPEED; G.matrix_avg_speed = DEFAULT_AVG_SPEED; G.matrix_min_trail = DEFAULT_MIN_TRAIL; G.matrix_max_trail = DEFAULT_MAX_TRAIL; G.matrix_min_density = DEFAULT_MIN_DENSITY; G.matrix_max_density = DEFAULT_MAX_DENSITY; G.matrix_layer_near_density = LAYER_NEAR_DENSITY; G.matrix_layer_far_density = LAYER_FAR_DENSITY; G.matrix_font_size = 14; G.matrix_depth_layers = DEFAULT_DEPTH_LAYERS; G.matrix_fade_frames = S.fade_frames; snprintf(G.matrix_color, sizeof(G.matrix_color), "green"); if (state_home && state_home[0] != '\0') snprintf(G.log_file, sizeof(G.log_file), "%s/oledsaver/oledsaver.log", state_home); else if (home && home[0] != '\0') snprintf(G.log_file, sizeof(G.log_file), "%s/.local/state/oledsaver/oledsaver.log", home); else snprintf(G.log_file, sizeof(G.log_file), "/tmp/oledsaver.log"); G.lock_cmd[0] = '\0'; } static void mgr_load_config(void) { if (G.config_file[0] == '\0') { const char *config_home = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); char candidate[MAX_PATH_LEN]; if (config_home && config_home[0] != '\0') { snprintf(candidate, sizeof(candidate), "%s/oledsaver.conf", config_home); if (access(candidate, R_OK) == 0) snprintf(G.config_file, sizeof(G.config_file), "%s", candidate); } if (G.config_file[0] == '\0' && home && home[0] != '\0') { snprintf(candidate, sizeof(candidate), "%s/.config/oledsaver.conf", home); if (access(candidate, R_OK) == 0) snprintf(G.config_file, sizeof(G.config_file), "%s", candidate); } if (G.config_file[0] == '\0') snprintf(G.config_file, sizeof(G.config_file), "/etc/oledsaver.conf"); } struct stat st; if (stat(G.config_file, &st) < 0) { LOG_WARNING("Config not found: %s, using defaults", G.config_file); mgr_set_defaults(); return; } double mtime = st.st_mtim.tv_sec + st.st_mtim.tv_nsec / 1e9; if (mtime <= G.config_mtime) return; FILE *fp = fopen(G.config_file, "r"); if (!fp) { LOG_ERROR("Cannot open config: %s", G.config_file); return; } G.config_mtime = mtime; for (int i = 0; i < G.output_count; i++) { G.outputs[i].timeout_override = -1; G.outputs[i].color[0] = '\0'; G.outputs[i].min_speed = -1.0; G.outputs[i].max_speed = -1.0; G.outputs[i].min_trail = -1; G.outputs[i].max_trail = -1; G.outputs[i].min_density = -1.0; G.outputs[i].max_density = -1.0; } char section[64] = ""; char line[1024]; while (fgets(line, sizeof(line), fp)) { mgr_trim(line); if (line[0] == '\0' || line[0] == '#' || line[0] == ';') continue; if (line[0] == '[') { char *end = strchr(line, ']'); if (end) { *end = '\0'; snprintf(section, sizeof(section), "%s", line + 1); } continue; } char *eq = strchr(line, '='); if (!eq) continue; *eq = '\0'; char key[256], val[768]; snprintf(key, sizeof(key), "%s", line); snprintf(val, sizeof(val), "%s", eq + 1); mgr_trim(key); mgr_trim(val); if (strcmp(section, "global") == 0) { if (strcmp(key, "TIMEOUT") == 0) G.global_timeout = atoi(val); else if (strcmp(key, "CHECK_INTERVAL") == 0) G.check_interval = atoi(val); else if (strcmp(key, "FOCUSED_TIMEOUT") == 0) G.focused_timeout = atoi(val); else if (strcmp(key, "LOG_FILE") == 0) mgr_expand_tilde(val, G.log_file, sizeof(G.log_file)); else if (strcmp(key, "LOCK_CMD") == 0) snprintf(G.lock_cmd, sizeof(G.lock_cmd), "%s", val); } else if (strcmp(section, "matrix") == 0) { if (strcmp(key, "CMD") == 0) snprintf(G.matrix_cmd, sizeof(G.matrix_cmd), "%s", val); else if (strcmp(key, "COLOR") == 0) snprintf(G.matrix_color, sizeof(G.matrix_color), "%s", val); else if (strcmp(key, "MIN_SPEED") == 0) G.matrix_min_speed = atof(val); else if (strcmp(key, "MAX_SPEED") == 0) G.matrix_max_speed = atof(val); else if (strcmp(key, "AVG_SPEED") == 0) G.matrix_avg_speed = atof(val); else if (strcmp(key, "MIN_TRAIL") == 0) G.matrix_min_trail = atoi(val); else if (strcmp(key, "MAX_TRAIL") == 0) G.matrix_max_trail = atoi(val); else if (strcmp(key, "MIN_DENSITY") == 0) G.matrix_min_density = atof(val); else if (strcmp(key, "MAX_DENSITY") == 0) G.matrix_max_density = atof(val); else if (strcmp(key, "FONT_SIZE") == 0) G.matrix_font_size = atoi(val); else if (strcmp(key, "LAYER_NEAR_DENSITY") == 0) G.matrix_layer_near_density = atof(val); else if (strcmp(key, "LAYER_FAR_DENSITY") == 0) G.matrix_layer_far_density = atof(val); else if (strcmp(key, "DEPTH_LAYERS") == 0) { G.matrix_depth_layers = atoi(val); if (G.matrix_depth_layers < MIN_DEPTH_LAYERS) G.matrix_depth_layers = MIN_DEPTH_LAYERS; if (G.matrix_depth_layers > MAX_DEPTH_LAYERS) G.matrix_depth_layers = MAX_DEPTH_LAYERS; } else if (strcmp(key, "FADE_FRAMES") == 0) { G.matrix_fade_frames = atoi(val); if (G.matrix_fade_frames < 1) G.matrix_fade_frames = 1; if (G.matrix_fade_frames > 300) G.matrix_fade_frames = 300; } } else if (strcmp(section, "output_timeouts") == 0) { for (int i = 0; i < G.output_count; i++) { if (strcmp(G.outputs[i].name, key) == 0) G.outputs[i].timeout_override = atoi(val); } } else if (strcmp(section, "output_colors") == 0) { for (int i = 0; i < G.output_count; i++) { if (strcmp(G.outputs[i].name, key) == 0) snprintf(G.outputs[i].color, sizeof(G.outputs[i].color), "%s", val); } } } fclose(fp); LOG_INFO("Config loaded: FOCUSED_TIMEOUT=%ds TIMEOUT=%ds CHECK_INTERVAL=%ds COLOR=%s FONT_SIZE=%d DEPTH_LAYERS=%d speed=%.1f-%.1f trail=%d-%d density=%.2f-%.2f", G.focused_timeout, G.global_timeout, G.check_interval, G.matrix_color, G.matrix_font_size, G.matrix_depth_layers, G.matrix_min_speed, G.matrix_max_speed, G.matrix_min_trail, G.matrix_max_trail, G.matrix_min_density, G.matrix_max_density); } /* ── Output helpers ────────────────────────────────────────────────── */ static mgr_output_info_t *mgr_find_output(const char *name) { for (int i = 0; i < G.output_count; i++) { if (strcmp(G.outputs[i].name, name) == 0) return &G.outputs[i]; } return NULL; } static int mgr_ipc_response_has_error(struct json_object *resp) { struct json_object *error; if (json_object_object_get_ex(resp, "error", &error)) return 1; struct json_object *result; if (json_object_object_get_ex(resp, "result", &result)) { const char *status = json_object_get_string(result); if (status && strcmp(status, "ok") != 0) return 1; } return 0; } static int mgr_get_cursor_status_from_ipc(char *output, size_t output_len, int *hidden) { if (output_len > 0) output[0] = '\0'; if (hidden) *hidden = -1; struct json_object *resp = mgr_ipc_request("oledsaver/cursor_status", "{}"); if (!resp) return 0; int ok = 0; if (mgr_ipc_response_has_error(resp)) goto out; struct json_object *valid; if (json_object_object_get_ex(resp, "valid", &valid) && !json_object_get_boolean(valid)) goto out; struct json_object *hidden_obj; if (hidden && json_object_object_get_ex(resp, "hidden", &hidden_obj)) *hidden = json_object_get_boolean(hidden_obj) ? 1 : 0; struct json_object *name_obj; if (json_object_object_get_ex(resp, "output", &name_obj)) { const char *name = json_object_get_string(name_obj); if (name && name[0] != '\0') { snprintf(output, output_len, "%s", name); ok = 1; } } out: json_object_put(resp); return ok; } static int mgr_set_wayfire_cursor_hidden(int hidden) { struct json_object *resp = mgr_ipc_request( hidden ? "oledsaver/hide_cursor" : "oledsaver/unhide_cursor", "{}"); if (!resp) return 0; int ok = !mgr_ipc_response_has_error(resp); json_object_put(resp); return ok; } static void mgr_release_wayfire_cursor(void) { pthread_mutex_lock(&G.lock); int was_hidden = G.wayfire_cursor_hidden; pthread_mutex_unlock(&G.lock); if (!was_hidden) return; int ok = mgr_set_wayfire_cursor_hidden(0); pthread_mutex_lock(&G.lock); G.wayfire_cursor_hidden = 0; pthread_mutex_unlock(&G.lock); if (ok) LOG_INFO("Cursor unhidden"); else LOG_WARNING("Cursor unhide request failed"); } static void mgr_update_wayfire_cursor(void) { char cursor_output[MAX_OUTPUT_NAME]; int actual_hidden = -1; if (!mgr_get_cursor_status_from_ipc(cursor_output, sizeof(cursor_output), &actual_hidden)) { pthread_mutex_lock(&G.lock); int should_warn = !G.wayfire_cursor_warned; G.wayfire_cursor_warned = 1; pthread_mutex_unlock(&G.lock); if (should_warn) LOG_WARNING("Cursor tracking unavailable; load Wayfire plugin oledsaver-cursor"); mgr_release_wayfire_cursor(); return; } pthread_mutex_lock(&G.lock); G.wayfire_cursor_warned = 0; if (actual_hidden >= 0) G.wayfire_cursor_hidden = actual_hidden; mgr_output_info_t *o = mgr_find_output(cursor_output); int should_hide = o && strcmp(o->state, "off") == 0; int is_hidden = G.wayfire_cursor_hidden; pthread_mutex_unlock(&G.lock); if (should_hide == is_hidden) return; if (mgr_set_wayfire_cursor_hidden(should_hide)) { pthread_mutex_lock(&G.lock); G.wayfire_cursor_hidden = should_hide; pthread_mutex_unlock(&G.lock); LOG_INFO("Cursor %s on %s", should_hide ? "hidden" : "unhidden", cursor_output); } else { pthread_mutex_lock(&G.lock); int should_warn = !G.wayfire_cursor_warned; G.wayfire_cursor_warned = 1; pthread_mutex_unlock(&G.lock); if (should_warn) LOG_WARNING("Cursor %s request failed", should_hide ? "hide" : "unhide"); } } static int mgr_get_output_timeout(mgr_output_info_t *o) { if (o->timeout_override > 0) return o->timeout_override; return G.global_timeout; } static const char *mgr_get_output_color(mgr_output_info_t *o) { static char hex_buf[8]; const char *color = o->color[0] ? o->color : G.matrix_color; /* Per-trail modes: pass through to renderer as-is */ if (strcasecmp(color, "random_trail_named") == 0 || strcasecmp(color, "random_trail_hex") == 0) return color; /* Per-output random: resolve here so each output gets a different color */ if (strcasecmp(color, "random") == 0 || strcasecmp(color, "random_named") == 0) return mgr_random_color(); if (strcasecmp(color, "random_hex") == 0) { snprintf(hex_buf, sizeof(hex_buf), "#%02x%02x%02x", rand() % 256, rand() % 256, rand() % 256); return hex_buf; } return color; } static double mgr_get_val_d(double per_output, double global) { return per_output >= 0.0 ? per_output : global; } static int mgr_get_val_i(int per_output, int global) { return per_output >= 0 ? per_output : global; } static void mgr_init_output(mgr_output_info_t *out, const char *name, double now) { memset(out, 0, sizeof(*out)); snprintf(out->name, sizeof(out->name), "%s", name); out->last_input = now; strcpy(out->state, "on"); out->timeout_override = -1; out->min_speed = -1.0; out->max_speed = -1.0; out->min_trail = -1; out->max_trail = -1; out->min_density = -1.0; out->max_density = -1.0; } typedef struct { struct wl_display *display; struct wl_registry *registry; struct zxdg_output_manager_v1 *xdg_output_manager; struct wl_list outputs; } mgr_outputs_wayland_t; typedef struct { struct wl_output *output; struct zxdg_output_v1 *xdg_output; char name[MAX_OUTPUT_NAME]; int width; int height; int scale; struct wl_list link; } mgr_wayland_output_info_t; static void mgr_output_set_name(mgr_wayland_output_info_t *info, const char *name) { if (!name || name[0] == '\0') return; snprintf(info->name, sizeof(info->name), "%s", name); } static void mgr_xdg_output_handle_logical_position(void *data, struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { (void)data; (void)xdg_output; (void)x; (void)y; } static void mgr_xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t w, int32_t h) { (void)xdg_output; mgr_wayland_output_info_t *info = data; info->width = w; info->height = h; } static void mgr_xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) { (void)data; (void)xdg_output; } static void mgr_xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { (void)xdg_output; mgr_output_set_name(data, name); } static void mgr_xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *desc) { (void)data; (void)xdg_output; (void)desc; } static const struct zxdg_output_v1_listener mgr_xdg_output_listener = { .logical_position = mgr_xdg_output_handle_logical_position, .logical_size = mgr_xdg_output_handle_logical_size, .done = mgr_xdg_output_handle_done, .name = mgr_xdg_output_handle_name, .description = mgr_xdg_output_handle_description, }; static void mgr_output_handle_geometry(void *data, struct wl_output *output, int32_t x, int32_t y, int32_t pw, int32_t ph, int32_t subpixel, const char *make, const char *model, int32_t transform) { (void)data; (void)output; (void)x; (void)y; (void)pw; (void)ph; (void)subpixel; (void)make; (void)model; (void)transform; } static void mgr_output_handle_mode(void *data, struct wl_output *output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { (void)output; (void)refresh; mgr_wayland_output_info_t *info = data; if (flags & WL_OUTPUT_MODE_CURRENT) { info->width = width; info->height = height; } } static void mgr_output_handle_done(void *data, struct wl_output *output) { (void)data; (void)output; } static void mgr_output_handle_scale(void *data, struct wl_output *output, int32_t factor) { (void)output; mgr_wayland_output_info_t *info = data; info->scale = factor; } static void mgr_output_handle_name(void *data, struct wl_output *output, const char *name) { (void)output; mgr_output_set_name(data, name); } static void mgr_output_handle_description(void *data, struct wl_output *output, const char *desc) { (void)data; (void)output; (void)desc; } static const struct wl_output_listener mgr_output_listener = { .geometry = mgr_output_handle_geometry, .mode = mgr_output_handle_mode, .done = mgr_output_handle_done, .scale = mgr_output_handle_scale, .name = mgr_output_handle_name, .description = mgr_output_handle_description, }; static void mgr_outputs_registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { mgr_outputs_wayland_t *ctx = data; if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && !ctx->xdg_output_manager) { ctx->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, version < 3 ? version : 3); } else if (strcmp(interface, wl_output_interface.name) == 0) { mgr_wayland_output_info_t *info = calloc(1, sizeof(*info)); if (!info) return; info->output = wl_registry_bind(registry, name, &wl_output_interface, version < 4 ? version : 4); info->scale = 1; wl_output_add_listener(info->output, &mgr_output_listener, info); wl_list_insert(&ctx->outputs, &info->link); } } static void mgr_outputs_registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data; (void)registry; (void)name; } static const struct wl_registry_listener mgr_outputs_registry_listener = { .global = mgr_outputs_registry_handle_global, .global_remove = mgr_outputs_registry_handle_global_remove, }; static void mgr_outputs_wayland_cleanup(mgr_outputs_wayland_t *ctx) { mgr_wayland_output_info_t *info, *tmp; wl_list_for_each_safe(info, tmp, &ctx->outputs, link) { if (info->xdg_output) zxdg_output_v1_destroy(info->xdg_output); if (info->output) wl_output_release(info->output); wl_list_remove(&info->link); free(info); } if (ctx->xdg_output_manager) zxdg_output_manager_v1_destroy(ctx->xdg_output_manager); if (ctx->registry) wl_registry_destroy(ctx->registry); if (ctx->display) wl_display_disconnect(ctx->display); } static void mgr_refresh_outputs(void) { mgr_outputs_wayland_t ctx = {0}; wl_list_init(&ctx.outputs); ensure_wayland_display(); ctx.display = wl_display_connect(NULL); if (!ctx.display) { LOG_ERROR("Output refresh: cannot connect to Wayland display"); return; } double now = mgr_monotime(); ctx.registry = wl_display_get_registry(ctx.display); wl_registry_add_listener(ctx.registry, &mgr_outputs_registry_listener, &ctx); wl_display_roundtrip(ctx.display); if (ctx.xdg_output_manager) { mgr_wayland_output_info_t *info; wl_list_for_each(info, &ctx.outputs, link) { info->xdg_output = zxdg_output_manager_v1_get_xdg_output( ctx.xdg_output_manager, info->output); zxdg_output_v1_add_listener(info->xdg_output, &mgr_xdg_output_listener, info); } wl_display_roundtrip(ctx.display); } pthread_mutex_lock(&G.lock); mgr_wayland_output_info_t *info; wl_list_for_each(info, &ctx.outputs, link) { if (info->name[0] == '\0') continue; if (!mgr_find_output(info->name) && G.output_count < MAX_OUTPUTS) { mgr_init_output(&G.outputs[G.output_count++], info->name, now); LOG_INFO("Discovered output: %s", info->name); } } pthread_mutex_unlock(&G.lock); mgr_outputs_wayland_cleanup(&ctx); } static const char *mgr_get_focused_output_from_ipc(void) { static char buf[MAX_OUTPUT_NAME]; struct json_object *resp = mgr_ipc_request("window-rules/get-focused-output", "{}"); if (!resp) return NULL; const char *result = NULL; struct json_object *status; if (json_object_object_get_ex(resp, "result", &status) && strcmp(json_object_get_string(status), "ok") != 0) goto out; struct json_object *info, *name; if (json_object_object_get_ex(resp, "info", &info) && json_object_object_get_ex(info, "name", &name)) { snprintf(buf, sizeof(buf), "%s", json_object_get_string(name)); result = buf; } out: json_object_put(resp); return result; } static void mgr_update_focused_output(const char *focused) { if (!focused || focused[0] == '\0') return; pthread_mutex_lock(&G.lock); bool known = mgr_find_output(focused) != NULL; pthread_mutex_unlock(&G.lock); if (!known) mgr_refresh_outputs(); pthread_mutex_lock(&G.lock); char old[MAX_OUTPUT_NAME]; snprintf(old, sizeof(old), "%s", G.focused_output); snprintf(G.focused_output, sizeof(G.focused_output), "%s", focused); if (strcmp(old, focused) != 0) { /* * Focus shift counts as fresh attention on both monitors: the new one * is being looked at, and the old one's unfocused timeout starts now. */ double now = mgr_monotime(); mgr_output_info_t *new_o = mgr_find_output(focused); mgr_output_info_t *old_o = mgr_find_output(old); if (new_o) new_o->last_input = now; if (old_o) old_o->last_input = now; LOG_INFO("Focus moved: %s -> %s", old, focused); } pthread_mutex_unlock(&G.lock); } /* ── Screensaver management ────────────────────────────────────────── */ static int mgr_pids_alive(mgr_output_info_t *o) { if (o->pid_count == 0) return 0; for (int i = 0; i < o->pid_count; i++) { if (!mgr_pid_alive(o->pids[i])) return 0; } return 1; } static void mgr_kill_pids(mgr_output_info_t *o) { for (int i = 0; i < o->pid_count; i++) { if (o->pids[i] > 0) kill(o->pids[i], SIGTERM); } o->pid_count = 0; } /* * Get path to current binary via /proc/self/exe. */ static const char *mgr_get_self_exe(void) { static char path[MAX_PATH_LEN]; ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1); if (n > 0) { path[n] = '\0'; return path; } /* Fallback: hope "oledsaver" is in PATH */ return "oledsaver"; } static void mgr_blank_output(const char *output_name) { pthread_mutex_lock(&G.lock); mgr_output_info_t *o = mgr_find_output(output_name); if (!o) { pthread_mutex_unlock(&G.lock); return; } if (strcmp(o->state, "off") == 0) { if (mgr_pids_alive(o)) { pthread_mutex_unlock(&G.lock); return; } G.last_blank_time = mgr_monotime(); LOG_INFO("[%s] saver died, restarting", output_name); mgr_kill_pids(o); } else { G.last_blank_time = mgr_monotime(); } const char *color = mgr_get_output_color(o); double min_speed = mgr_get_val_d(o->min_speed, G.matrix_min_speed); double max_speed = mgr_get_val_d(o->max_speed, G.matrix_max_speed); int min_trail = mgr_get_val_i(o->min_trail, G.matrix_min_trail); int max_trail = mgr_get_val_i(o->max_trail, G.matrix_max_trail); double min_density = mgr_get_val_d(o->min_density, G.matrix_min_density); double max_density = mgr_get_val_d(o->max_density, G.matrix_max_density); int depth_layers = G.matrix_depth_layers; pthread_mutex_unlock(&G.lock); const char *bin = mgr_get_self_exe(); char font_size_str[16], min_speed_str[16], max_speed_str[16], avg_speed_str[16]; char min_trail_str[16], max_trail_str[16]; char min_density_str[16], max_density_str[16]; char depth_layers_str[16], fade_frames_str[16]; char layer_near_density_str[16], layer_far_density_str[16]; snprintf(font_size_str, sizeof(font_size_str), "%d", G.matrix_font_size); snprintf(min_speed_str, sizeof(min_speed_str), "%.2f", min_speed); snprintf(max_speed_str, sizeof(max_speed_str), "%.2f", max_speed); snprintf(avg_speed_str, sizeof(avg_speed_str), "%.2f", G.matrix_avg_speed); snprintf(min_trail_str, sizeof(min_trail_str), "%d", min_trail); snprintf(max_trail_str, sizeof(max_trail_str), "%d", max_trail); snprintf(min_density_str, sizeof(min_density_str), "%.2f", min_density); snprintf(max_density_str, sizeof(max_density_str), "%.2f", max_density); snprintf(depth_layers_str, sizeof(depth_layers_str), "%d", depth_layers); snprintf(fade_frames_str, sizeof(fade_frames_str), "%d", G.matrix_fade_frames); snprintf(layer_near_density_str, sizeof(layer_near_density_str), "%.2f", G.matrix_layer_near_density); snprintf(layer_far_density_str, sizeof(layer_far_density_str), "%.2f", G.matrix_layer_far_density); pid_t pid = fork(); if (pid < 0) { LOG_ERROR("[%s] fork failed: %s", output_name, strerror(errno)); return; } if (pid == 0) { /* Child: exec self in render mode */ int devnull = open("/dev/null", O_RDWR); if (devnull >= 0) { dup2(devnull, STDOUT_FILENO); dup2(devnull, STDERR_FILENO); close(devnull); } execl(bin, "oledsaver", "--render", "--output", output_name, "--color", color, "--font-size", font_size_str, "--min-speed", min_speed_str, "--max-speed", max_speed_str, "--avg-speed", avg_speed_str, "--min-trail", min_trail_str, "--max-trail", max_trail_str, "--min-density", min_density_str, "--max-density", max_density_str, "--depth-layers", depth_layers_str, "--fade-frames", fade_frames_str, "--layer-near-density", layer_near_density_str, "--layer-far-density", layer_far_density_str, (char *)NULL); _exit(127); } /* Parent: wait briefly to check if it exits immediately */ usleep(500000); int status; pid_t w = waitpid(pid, &status, WNOHANG); if (w > 0) { LOG_WARNING("[%s] renderer exited immediately", output_name); return; } pthread_mutex_lock(&G.lock); o = mgr_find_output(output_name); if (o) { o->pids[0] = pid; o->pid_count = 1; strcpy(o->state, "off"); LOG_INFO("[%s] matrix screensaver active (PID %d, color=%s, speed=%.1f-%.1f, depth=%d)", output_name, pid, color, min_speed, max_speed, depth_layers); } pthread_mutex_unlock(&G.lock); } static void mgr_unblank_output(const char *output_name) { pthread_mutex_lock(&G.lock); mgr_output_info_t *o = mgr_find_output(output_name); if (!o || strcmp(o->state, "on") == 0) { pthread_mutex_unlock(&G.lock); return; } /* Send SIGUSR1 to trigger fade-out in renderer processes */ for (int i = 0; i < o->pid_count; i++) { if (o->pids[i] > 0) kill(o->pids[i], SIGUSR1); } /* Save pids and count for waiting outside the lock */ pid_t pids[MAX_PIDS]; int pid_count = o->pid_count; for (int i = 0; i < pid_count; i++) pids[i] = o->pids[i]; pthread_mutex_unlock(&G.lock); /* Wait up to 3 seconds for renderers to exit (fade-out + fade-to-desktop) */ int wait_ms = 3000; int poll_interval_ms = 50; bool all_exited = false; for (int elapsed = 0; elapsed < wait_ms; elapsed += poll_interval_ms) { all_exited = true; for (int i = 0; i < pid_count; i++) { if (pids[i] <= 0) continue; int status; pid_t ret = waitpid(pids[i], &status, WNOHANG); if (ret > 0) { pids[i] = 0; /* mark as reaped */ } else if (ret == 0) { all_exited = false; /* still running */ } else { pids[i] = 0; /* error or already reaped */ } } if (all_exited) break; usleep((useconds_t)(poll_interval_ms * 1000)); } /* Fallback: SIGTERM any remaining processes */ pthread_mutex_lock(&G.lock); if (!all_exited) { for (int i = 0; i < pid_count; i++) { if (pids[i] > 0) kill(pids[i], SIGTERM); } } /* Clear the pid list */ o->pid_count = 0; strcpy(o->state, "on"); pthread_mutex_unlock(&G.lock); LOG_INFO("[%s] unblanked", output_name); } /* ── Focus watcher thread ──────────────────────────────────────────── */ static const char *mgr_focus_name_from_event(struct json_object *event, char *buf, size_t bufsz) { struct json_object *event_name_obj; const char *event_name = NULL; if (json_object_object_get_ex(event, "event", &event_name_obj)) event_name = json_object_get_string(event_name_obj); if (!event_name) return NULL; if (strcmp(event_name, "output-added") == 0 || strcmp(event_name, "output-removed") == 0) { mgr_refresh_outputs(); return NULL; } if (strcmp(event_name, "output-gain-focus") == 0) { struct json_object *output, *name; if (json_object_object_get_ex(event, "output", &output) && json_object_object_get_ex(output, "name", &name)) { snprintf(buf, bufsz, "%s", json_object_get_string(name)); return buf; } return NULL; } if (strcmp(event_name, "view-focused") == 0) { struct json_object *view, *name; if (json_object_object_get_ex(event, "view", &view) && json_object_object_get_ex(view, "output-name", &name)) { snprintf(buf, bufsz, "%s", json_object_get_string(name)); return buf; } return mgr_get_focused_output_from_ipc(); } return NULL; } static void *mgr_focus_watcher(void *arg) { (void)arg; while (!G.stop) { int fd = mgr_ipc_connect(); if (fd < 0) { LOG_ERROR("Focus watcher: cannot connect to Wayfire IPC"); sleep(2); continue; } if (mgr_ipc_send_method(fd, "window-rules/events/watch", "{\"events\":[\"view-focused\",\"output-gain-focus\"," "\"output-added\",\"output-removed\"]}") < 0) { close(fd); sleep(2); continue; } char *resp = NULL; if (mgr_ipc_recv(fd, &resp) < 0) { free(resp); close(fd); sleep(2); continue; } struct json_object *ack = json_tokener_parse(resp); free(resp); if (ack) { struct json_object *status; if (json_object_object_get_ex(ack, "result", &status) && strcmp(json_object_get_string(status), "ok") != 0) { LOG_ERROR("Focus watcher: Wayfire event watch rejected: %s", json_object_to_json_string_ext( ack, JSON_C_TO_STRING_PLAIN)); json_object_put(ack); close(fd); sleep(2); continue; } json_object_put(ack); } LOG_INFO("Focus watcher: using Wayfire IPC events"); while (!G.stop) { char *payload = NULL; if (mgr_ipc_recv(fd, &payload) < 0) { free(payload); break; } struct json_object *event = json_tokener_parse(payload); free(payload); if (!event) continue; char focused_buf[MAX_OUTPUT_NAME]; const char *focused = mgr_focus_name_from_event(event, focused_buf, sizeof(focused_buf)); json_object_put(event); if (focused) { mgr_update_focused_output(focused); } } close(fd); } return NULL; } /* ── Idle watcher thread ───────────────────────────────────────────── */ typedef struct { struct wl_display *display; struct wl_registry *registry; struct wl_seat *seat; struct ext_idle_notifier_v1 *notifier; struct ext_idle_notification_v1 *notification; uint32_t notifier_version; uint32_t seat_version; } mgr_idle_wayland_t; static void mgr_idle_handle_idled(void *data, struct ext_idle_notification_v1 *notification) { (void)data; (void)notification; pthread_mutex_lock(&G.lock); G.seat_idle = 1; pthread_mutex_unlock(&G.lock); } static void mgr_idle_handle_resumed(void *data, struct ext_idle_notification_v1 *notification) { (void)data; (void)notification; double now = mgr_monotime(); pthread_mutex_lock(&G.lock); G.seat_idle = 0; if ((now - G.last_blank_time) >= 3.0) { for (int i = 0; i < G.output_count; i++) G.outputs[i].last_input = now; } pthread_mutex_unlock(&G.lock); } static const struct ext_idle_notification_v1_listener mgr_idle_listener = { .idled = mgr_idle_handle_idled, .resumed = mgr_idle_handle_resumed, }; static void mgr_idle_registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { mgr_idle_wayland_t *idle = data; if (strcmp(interface, wl_seat_interface.name) == 0 && !idle->seat) { idle->seat_version = version < 7 ? version : 7; idle->seat = wl_registry_bind(registry, name, &wl_seat_interface, idle->seat_version); } else if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0 && !idle->notifier) { idle->notifier_version = version < 2 ? version : 2; idle->notifier = wl_registry_bind(registry, name, &ext_idle_notifier_v1_interface, idle->notifier_version); } } static void mgr_idle_registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { (void)data; (void)registry; (void)name; } static const struct wl_registry_listener mgr_idle_registry_listener = { .global = mgr_idle_registry_handle_global, .global_remove = mgr_idle_registry_handle_global_remove, }; static void mgr_idle_wayland_cleanup(mgr_idle_wayland_t *idle) { if (idle->notification) ext_idle_notification_v1_destroy(idle->notification); if (idle->notifier) ext_idle_notifier_v1_destroy(idle->notifier); if (idle->seat) { if (idle->seat_version >= WL_SEAT_RELEASE_SINCE_VERSION) wl_seat_release(idle->seat); else wl_seat_destroy(idle->seat); } if (idle->registry) wl_registry_destroy(idle->registry); if (idle->display) wl_display_disconnect(idle->display); memset(idle, 0, sizeof(*idle)); } static void *mgr_idle_watcher(void *arg) { (void)arg; pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL); while (!G.stop) { mgr_idle_wayland_t idle = {0}; idle.display = wl_display_connect(NULL); if (!idle.display) { LOG_ERROR("Idle watcher: cannot connect to Wayland display"); sleep(2); continue; } idle.registry = wl_display_get_registry(idle.display); wl_registry_add_listener(idle.registry, &mgr_idle_registry_listener, &idle); wl_display_roundtrip(idle.display); if (!idle.notifier || !idle.seat) { LOG_ERROR("Idle watcher: compositor lacks ext-idle-notify-v1 or wl_seat"); mgr_idle_wayland_cleanup(&idle); sleep(5); continue; } idle.notification = ext_idle_notifier_v1_get_idle_notification( idle.notifier, 2000, idle.seat); ext_idle_notification_v1_add_listener(idle.notification, &mgr_idle_listener, NULL); wl_display_flush(idle.display); LOG_INFO("Idle watcher: using ext-idle-notify-v1"); while (!G.stop) { int rc = wl_display_dispatch(idle.display); if (rc < 0) { LOG_ERROR("Idle watcher: Wayland dispatch failed"); break; } } mgr_idle_wayland_cleanup(&idle); if (!G.stop) sleep(2); } return NULL; } /* ── Manager signal handling ───────────────────────────────────────── */ static void mgr_signal_handler(int sig) { if (sig == SIGUSR1) G.activate_all = 1; else if (sig == SIGUSR2) G.lock_now = 1; else G.stop = 1; } /* ── Lock command ──────────────────────────────────────────────────── */ static void mgr_run_lock_cmd(void) { if (G.lock_cmd[0] == '\0') { LOG_WARNING("Lock requested but no LOCK_CMD configured"); return; } LOG_INFO("Running lock command: %s", G.lock_cmd); pid_t pid = fork(); if (pid == 0) { setsid(); execlp("/bin/sh", "sh", "-c", G.lock_cmd, (char *)NULL); _exit(127); } else if (pid > 0) { LOG_INFO("Lock command started (PID %d)", pid); } else { LOG_ERROR("Failed to fork for lock command: %s", strerror(errno)); } } /* * Blank all outputs immediately (for activate/lock triggers). */ static void mgr_activate_all_outputs(void) { pthread_mutex_lock(&G.lock); G.seat_idle = 1; G.last_blank_time = mgr_monotime(); int n = G.output_count; char names[MAX_OUTPUTS][MAX_OUTPUT_NAME]; for (int i = 0; i < n; i++) snprintf(names[i], MAX_OUTPUT_NAME, "%s", G.outputs[i].name); pthread_mutex_unlock(&G.lock); for (int i = 0; i < n; i++) mgr_blank_output(names[i]); LOG_INFO("All outputs activated (screensaver forced on)"); } /* ── PID file for signal delivery ──────────────────────────────────── */ static void mgr_write_pidfile(void) { char path[MAX_PATH_LEN]; mgr_runtime_path(path, sizeof(path), "pid"); FILE *f = fopen(path, "w"); if (f) { fprintf(f, "%d\n", getpid()); fclose(f); } } static void mgr_remove_pidfile(void) { char path[MAX_PATH_LEN]; mgr_runtime_path(path, sizeof(path), "pid"); unlink(path); } static pid_t mgr_read_pidfile(void) { char path[MAX_PATH_LEN]; mgr_runtime_path(path, sizeof(path), "pid"); FILE *f = fopen(path, "r"); if (!f) return -1; int pid = 0; if (fscanf(f, "%d", &pid) != 1) pid = -1; fclose(f); return (pid_t)pid; } /* ── Zombie reaper ─────────────────────────────────────────────────── */ static void mgr_reap_children(void) { int status; while (waitpid(-1, &status, WNOHANG) > 0) ; } /* ── Manager main ──────────────────────────────────────────────────── */ static int manager_main(void) { srand(time(NULL)); memset(&G, 0, sizeof(G)); pthread_mutex_init(&G.lock, NULL); ensure_wayland_display(); mgr_set_defaults(); mgr_load_config(); /* Setup logging */ { char logdir[MAX_PATH_LEN]; snprintf(logdir, sizeof(logdir), "%s", G.log_file); char *slash = strrchr(logdir, '/'); if (slash) { *slash = '\0'; mgr_mkdirs(logdir); } } G.log_fp = fopen(G.log_file, "w"); if (!G.log_fp) fprintf(stderr, "Warning: cannot open log file %s: %s\n", G.log_file, strerror(errno)); /* Signal handlers */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = mgr_signal_handler; sigaction(SIGINT, &sa, NULL); sigaction(SIGTERM, &sa, NULL); sigaction(SIGUSR1, &sa, NULL); sigaction(SIGUSR2, &sa, NULL); signal(SIGPIPE, SIG_IGN); mgr_write_pidfile(); /* Initialize outputs, then re-read config for per-output overrides */ mgr_refresh_outputs(); G.config_mtime = 0; mgr_load_config(); const char *focused = mgr_get_focused_output_from_ipc(); pthread_mutex_lock(&G.lock); if (focused) snprintf(G.focused_output, sizeof(G.focused_output), "%s", focused); pthread_mutex_unlock(&G.lock); /* Log startup */ { char outlist[512] = ""; for (int i = 0; i < G.output_count; i++) { if (i > 0) strcat(outlist, ", "); strcat(outlist, G.outputs[i].name); } LOG_INFO("Started — outputs: [%s], focused: %s", outlist, G.focused_output); } /* Start threads */ pthread_t focus_tid, idle_tid; pthread_create(&focus_tid, NULL, mgr_focus_watcher, NULL); pthread_create(&idle_tid, NULL, mgr_idle_watcher, NULL); double last_config_check = 0; double last_cursor_check = 0; while (!G.stop) { double now = mgr_monotime(); if (now - last_config_check > 10) { mgr_load_config(); last_config_check = now; } mgr_reap_children(); /* Handle activate/lock signals */ if (G.lock_now) { G.lock_now = 0; G.activate_all = 0; /* lock implies activate */ mgr_activate_all_outputs(); mgr_run_lock_cmd(); } else if (G.activate_all) { G.activate_all = 0; mgr_activate_all_outputs(); } int inhibited = mgr_check_idle_inhibitors(); pthread_mutex_lock(&G.lock); char focused_buf[MAX_OUTPUT_NAME]; snprintf(focused_buf, sizeof(focused_buf), "%s", G.focused_output); int idle = G.seat_idle; if (!idle) { mgr_output_info_t *fo = mgr_find_output(focused_buf); if (fo) fo->last_input = now; } int n = G.output_count; char names[MAX_OUTPUTS][MAX_OUTPUT_NAME]; double last_inputs[MAX_OUTPUTS]; char states[MAX_OUTPUTS][8]; int timeouts[MAX_OUTPUTS]; int is_focused[MAX_OUTPUTS]; for (int i = 0; i < n; i++) { snprintf(names[i], MAX_OUTPUT_NAME, "%s", G.outputs[i].name); last_inputs[i] = G.outputs[i].last_input; snprintf(states[i], 8, "%s", G.outputs[i].state); is_focused[i] = (strcmp(names[i], focused_buf) == 0); if (is_focused[i]) timeouts[i] = G.focused_timeout; else timeouts[i] = mgr_get_output_timeout(&G.outputs[i]); } pthread_mutex_unlock(&G.lock); for (int i = 0; i < n; i++) { double idle_secs = now - last_inputs[i]; if (idle_secs >= timeouts[i] && !inhibited) { mgr_blank_output(names[i]); } else { if (strcmp(states[i], "off") == 0 && is_focused[i]) { mgr_unblank_output(names[i]); } } } if (now - last_cursor_check >= CURSOR_POLL_INTERVAL) { mgr_update_wayfire_cursor(); last_cursor_check = now; } for (int s = 0; s < G.check_interval * 10 && !G.stop; s++) { usleep(100000); double wait_now = mgr_monotime(); if (wait_now - last_cursor_check >= CURSOR_POLL_INTERVAL) { mgr_update_wayfire_cursor(); last_cursor_check = wait_now; } pthread_mutex_lock(&G.lock); int idle_now = G.seat_idle; int any_blanked = 0; if (!idle_now) { for (int i = 0; i < G.output_count; i++) { if (strcmp(G.outputs[i].state, "off") == 0) { any_blanked = 1; break; } } } pthread_mutex_unlock(&G.lock); if (!idle_now && any_blanked) break; } } /* Clean shutdown */ LOG_INFO("Shutting down..."); for (int i = 0; i < G.output_count; i++) mgr_unblank_output(G.outputs[i].name); mgr_release_wayfire_cursor(); pthread_cancel(focus_tid); pthread_cancel(idle_tid); mgr_remove_pidfile(); if (G.log_fp) fclose(G.log_fp); pthread_mutex_destroy(&G.lock); return 0; } /* =================================================================== * ENTRY POINT — dispatch to manager or renderer * =================================================================== */ int main(int argc, char **argv) { for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--render") == 0) { return renderer_main(argc, argv); } if (strcmp(argv[i], "--activate") == 0) { pid_t pid = mgr_read_pidfile(); if (pid <= 0 || kill(pid, 0) != 0) { fprintf(stderr, "oledsaver: no running manager found\n"); return 1; } kill(pid, SIGUSR1); printf("Sent activate to oledsaver (PID %d)\n", pid); return 0; } if (strcmp(argv[i], "--lock") == 0) { pid_t pid = mgr_read_pidfile(); if (pid <= 0 || kill(pid, 0) != 0) { fprintf(stderr, "oledsaver: no running manager found\n"); return 1; } kill(pid, SIGUSR2); printf("Sent lock to oledsaver (PID %d)\n", pid); return 0; } } /* No --render/--activate/--lock flag → manager mode */ (void)argc; return manager_main(); }