aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkj_sh6042026-06-05 16:07:29 -0400
committerkj_sh6042026-06-05 16:07:29 -0400
commit27bf89f9e509690c671bc9c303fdeeace5674c88 (patch)
treeffb33b9f796201b8ae9fae23b6c86ab9394d34b4
parent84bf1b569a76519e5758be4961cafdf2e86cd8ec (diff)
refactor: src/kj-boomer.c
-rw-r--r--src/kj-boomer.c809
1 files changed, 809 insertions, 0 deletions
diff --git a/src/kj-boomer.c b/src/kj-boomer.c
new file mode 100644
index 0000000..af4472a
--- /dev/null
+++ b/src/kj-boomer.c
@@ -0,0 +1,809 @@
+// TODO(20260426T215403): wayland compatible
+
+#define _POSIX_C_SOURCE 200809L
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include <time.h>
+#include <sys/stat.h>
+
+#define SCREENSHOT_IMPL
+#include "screenshot.h"
+#define LA_IMPL
+#include "la.h"
+#define CONFIG_IMPL
+#include "config.h"
+
+#include <X11/extensions/Xrandr.h>
+#include <X11/Xlib.h>
+#include <X11/Xutil.h>
+#include <GL/glew.h>
+#include <GL/glx.h>
+
+#define VERSION "20260426"
+
+static const char *VERTEX_SHADER_SOURCE =
+ "#version 130\n"
+ "in vec3 aPos;\n"
+ "in vec2 aTexCoord;\n"
+ "out vec2 texcoord;\n"
+ "uniform vec2 camera_pos;\n"
+ "uniform float camera_scale;\n"
+ "uniform vec2 window_size;\n"
+ "uniform vec2 screenshot_size;\n"
+ "vec3 to_world(vec3 v) {\n"
+ " vec2 ratio = vec2(\n"
+ " window_size.x / screenshot_size.x / camera_scale,\n"
+ " window_size.y / screenshot_size.y / camera_scale);\n"
+ " return vec3((v.x / screenshot_size.x * 2.0 - 1.0) / ratio.x,\n"
+ " (v.y / screenshot_size.y * 2.0 - 1.0) / ratio.y,\n"
+ " v.z);\n"
+ "}\n"
+ "void main() {\n"
+ " gl_Position = vec4(to_world((aPos - vec3(camera_pos * vec2(1.0, -1.0), 0.0))), 1.0);\n"
+ " texcoord = aTexCoord;\n"
+ "}\n";
+
+static const char *FRAGMENT_SHADER_SOURCE =
+ "#version 130\n"
+ "out mediump vec4 color;\n"
+ "in mediump vec2 texcoord;\n"
+ "uniform sampler2D tex;\n"
+ "uniform vec2 cursor_pos;\n"
+ "uniform vec2 window_size;\n"
+ "uniform float fl_shadow;\n"
+ "uniform float fl_radius;\n"
+ "uniform float camera_scale;\n"
+ "uniform float fl_feather;\n"
+ "uniform float mirror;\n"
+ "void main() {\n"
+ " vec4 cursor = vec4(cursor_pos.x, window_size.y - cursor_pos.y, 0.0, 1.0);\n"
+ " float dist = length(cursor - gl_FragCoord);\n"
+ " float radius_px = fl_radius * camera_scale;\n"
+ " float inner = radius_px * (1.0 - fl_feather);\n"
+ " float outer = radius_px;\n"
+ " float alpha = smoothstep(inner, outer, dist);\n"
+ " vec2 tc = texcoord;\n"
+ " if (mirror > 0.5) tc.x = 1.0 - tc.x;\n"
+ " color = mix(texture(tex, tc), vec4(0.0, 0.0, 0.0, 0.0), alpha * fl_shadow);\n"
+ "}\n";
+
+typedef struct {
+ Vec2f position;
+ Vec2f velocity;
+ float scale;
+ float delta_scale;
+ Vec2f scale_pivot;
+} Camera;
+
+typedef struct {
+ Vec2f curr;
+ Vec2f prev;
+ int drag;
+} Mouse;
+
+typedef struct {
+ int enabled;
+ float shadow;
+ float radius;
+ float delta_radius; // speed of radius change
+} Flashlight;
+
+typedef struct {
+ Display *display;
+ Window root;
+ Window window;
+ XVisualInfo *visual_info;
+ GLXContext gl_context;
+ Window original_focus_window; // window that had focus before we stole it
+ int screen_width;
+ int screen_height;
+ int refresh_rate;
+ int windowed;
+ Atom wm_delete_window;
+} X11Context;
+
+typedef struct {
+ GLuint program;
+ GLuint texture;
+ GLuint vao;
+ GLuint vbo;
+ GLuint ebo;
+ int screenshot_width;
+ int screenshot_height;
+} OpenGLContext;
+
+typedef struct {
+ Camera camera;
+ Mouse mouse;
+ Flashlight flashlight;
+ float dt; // delta time (seconds since last frame)
+ int running;
+ int mirror; // mirror image horizontally
+} State;
+
+typedef struct {
+ Config config;
+ State state;
+} App;
+
+// ================ config file
+
+static void mkdir_p(const char *path) {
+ char tmp[1024];
+ 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 char *get_default_config_path(void) {
+ const char *home = getenv("HOME");
+ if (!home) return NULL;
+ static char path[1024];
+ snprintf(path, sizeof(path), "%s/.config/boomer/config", home);
+ return path;
+}
+
+static void apply_config_value(Config *cfg, const char *key, float value) {
+ if (strcmp(key, "min_scale") == 0) cfg->min_scale = value;
+ else if (strcmp(key, "scroll_speed") == 0) cfg->scroll_speed = value;
+ else if (strcmp(key, "drag_friction") == 0) cfg->drag_friction = value;
+ else if (strcmp(key, "scale_friction") == 0) cfg->scale_friction = value;
+ else if (strcmp(key, "velocity_threshold") == 0) cfg->velocity_threshold = value;
+ else if (strcmp(key, "scale_change_threshold") == 0) cfg->scale_change_threshold = value;
+ else if (strcmp(key, "initial_radius") == 0) cfg->initial_radius = value;
+ else if (strcmp(key, "initial_delta_radius") == 0) cfg->initial_delta_radius = value;
+ else if (strcmp(key, "radius_damping") == 0) cfg->radius_damping = value;
+ else if (strcmp(key, "fade_speed") == 0) cfg->fade_speed = value;
+ else if (strcmp(key, "max_shadow_opacity") == 0) cfg->max_shadow_opacity = value;
+ else if (strcmp(key, "radius_change_threshold") == 0) cfg->radius_change_threshold = value;
+ else if (strcmp(key, "feather") == 0) cfg->feather = value;
+ else if (strcmp(key, "texture_filter") == 0) cfg->texture_filter = (int)value;
+}
+
+static void load_config_file(const char *filepath, Config *cfg) {
+ FILE *f = fopen(filepath, "r");
+ if (!f) return;
+
+ char line[256];
+ while (fgets(line, sizeof(line), f)) {
+ char *p = line;
+ while (*p == ' ' || *p == '\t') p++;
+ if (*p == '#' || *p == '\n' || *p == '\0') continue;
+
+ char key[64];
+ float value;
+ if (sscanf(p, "%63s = %f", key, &value) == 2) {
+ apply_config_value(cfg, key, value);
+ }
+ }
+ fclose(f);
+}
+
+static void write_default_config(const char *filepath) {
+ FILE *f = fopen(filepath, "w");
+ if (!f) {
+ fprintf(stderr, "error: could not write config to %s\n", filepath);
+ return;
+ }
+ Config *cfg = &default_config;
+ fprintf(f, "min_scale = %.1f\n", cfg->min_scale);
+ fprintf(f, "scroll_speed = %.1f\n", cfg->scroll_speed);
+ fprintf(f, "drag_friction = %.1f\n", cfg->drag_friction);
+ fprintf(f, "scale_friction = %.1f\n", cfg->scale_friction);
+ fprintf(f, "velocity_threshold = %.1f\n", cfg->velocity_threshold);
+ fprintf(f, "scale_change_threshold = %.2f\n", cfg->scale_change_threshold);
+ fprintf(f, "initial_radius = %.1f\n", cfg->initial_radius);
+ fprintf(f, "initial_delta_radius = %.1f\n", cfg->initial_delta_radius);
+ fprintf(f, "radius_damping = %.1f\n", cfg->radius_damping);
+ fprintf(f, "fade_speed = %.1f\n", cfg->fade_speed);
+ fprintf(f, "max_shadow_opacity = %.2f\n", cfg->max_shadow_opacity);
+ fprintf(f, "radius_change_threshold = %.2f\n", cfg->radius_change_threshold);
+ fprintf(f, "feather = %.2f\n", cfg->feather);
+ fprintf(f, "texture_filter = %d\n", cfg->texture_filter);
+ fclose(f);
+}
+
+// ================ x11
+
+static int x11_error_handler(Display *d, XErrorEvent *ev) {
+ char em[256];
+ XGetErrorText(d, ev->error_code, em, sizeof(em));
+ fprintf(stderr, "error: x11: %s\n", em);
+ return 0;
+}
+
+static int x11_init(X11Context *ctx) {
+ ctx->display = XOpenDisplay(NULL);
+ if (!ctx->display) {
+ fprintf(stderr, "error: failed to open display\n");
+ return 0;
+ }
+
+ XSetErrorHandler(x11_error_handler);
+
+ ctx->root = DefaultRootWindow(ctx->display);
+ XWindowAttributes root_attrs;
+ XGetWindowAttributes(ctx->display, ctx->root, &root_attrs);
+ ctx->screen_width = root_attrs.width;
+ ctx->screen_height = root_attrs.height;
+
+ XRRScreenConfiguration *sc = XRRGetScreenInfo(ctx->display, ctx->root);
+ ctx->refresh_rate = XRRConfigCurrentRate(sc);
+ XRRFreeScreenConfigInfo(sc);
+
+ printf("Screen: %dx%d @ %dHz\n",
+ ctx->screen_width, ctx->screen_height, ctx->refresh_rate);
+
+ return 1;
+}
+
+static int x11_check_glx(X11Context *ctx) {
+ int glx_major, glx_minor;
+ if (!glXQueryVersion(ctx->display, &glx_major, &glx_minor) ||
+ (glx_major == 1 && glx_minor < 3) || (glx_major < 1)) {
+ fprintf(stderr, "error: invalid glx version\n");
+ return 0;
+ }
+ printf("GLX version: %d.%d\n", glx_major, glx_minor);
+ return 1;
+}
+
+static int x11_create_window(X11Context *ctx, int windowed) {
+ ctx->windowed = windowed;
+ static int attrs[] = {GLX_RGBA, GLX_DEPTH_SIZE, 24, GLX_DOUBLEBUFFER, None};
+ ctx->visual_info = glXChooseVisual(ctx->display, 0, attrs);
+ if (!ctx->visual_info) {
+ fprintf(stderr, "error: no appropriate visual found\n");
+ return 0;
+ }
+ printf("Visual ID: 0x%lx\n", ctx->visual_info->visualid);
+
+ XSetWindowAttributes swa;
+ swa.colormap = XCreateColormap(ctx->display, ctx->root, ctx->visual_info->visual, AllocNone);
+ swa.event_mask = ButtonPressMask | ButtonReleaseMask | KeyPressMask | KeyReleaseMask |
+ PointerMotionMask | ExposureMask | ClientMessage;
+ unsigned long mask = CWColormap | CWEventMask;
+
+ if (!windowed) {
+ swa.override_redirect = 1;
+ swa.save_under = 1;
+ mask |= CWOverrideRedirect | CWSaveUnder;
+ }
+
+ ctx->window = XCreateWindow(ctx->display, ctx->root,
+ 0, 0, ctx->screen_width, ctx->screen_height, 0,
+ ctx->visual_info->depth, InputOutput,
+ ctx->visual_info->visual,
+ mask, &swa);
+
+ XStoreName(ctx->display, ctx->window, "boomer");
+ XClassHint class_hint = {"boomer", "Boomer"};
+ XSetClassHint(ctx->display, ctx->window, &class_hint);
+
+ if (windowed) {
+ ctx->wm_delete_window = XInternAtom(ctx->display, "WM_DELETE_WINDOW", False);
+ XSetWMProtocols(ctx->display, ctx->window, &ctx->wm_delete_window, 1);
+ }
+
+ XMapWindow(ctx->display, ctx->window);
+
+ ctx->gl_context = glXCreateContext(ctx->display, ctx->visual_info, NULL, GL_TRUE);
+ glXMakeCurrent(ctx->display, ctx->window, ctx->gl_context);
+
+ XGetInputFocus(ctx->display, &ctx->original_focus_window, &(int){0});
+
+ return 1;
+}
+
+static void x11_grab_focus(X11Context *ctx) {
+ if (!ctx->windowed)
+ XSetInputFocus(ctx->display, ctx->window, RevertToParent, CurrentTime);
+}
+
+static void x11_restore_focus(X11Context *ctx) {
+ XSetInputFocus(ctx->display, ctx->original_focus_window, RevertToParent, CurrentTime);
+ XSync(ctx->display, False);
+}
+
+static void x11_get_window_size(X11Context *ctx, int *w, int *h) {
+ XWindowAttributes wa;
+ XGetWindowAttributes(ctx->display, ctx->window, &wa);
+ *w = wa.width;
+ *h = wa.height;
+}
+
+static void x11_cleanup(X11Context *ctx) {
+ if (ctx->gl_context) {
+ glXMakeCurrent(ctx->display, None, NULL);
+ glXDestroyContext(ctx->display, ctx->gl_context);
+ }
+ if (ctx->window) XDestroyWindow(ctx->display, ctx->window);
+ if (ctx->visual_info) XFree(ctx->visual_info);
+ if (ctx->display) XCloseDisplay(ctx->display);
+}
+
+// ================ opengl
+
+static GLuint opengl_compile_shader(GLenum type, const char *source) {
+ GLuint shader = glCreateShader(type);
+ glShaderSource(shader, 1, &source, NULL);
+ glCompileShader(shader);
+
+ GLint success;
+ glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
+ if (!success) {
+ char log[512];
+ glGetShaderInfoLog(shader, 512, NULL, log);
+ fprintf(stderr, "error: shader compile: %s\n", log);
+ }
+ return shader;
+}
+
+static int opengl_init(OpenGLContext *gl, Screenshot *s) {
+ GLenum err = glewInit();
+ if (err != GLEW_OK) {
+ fprintf(stderr, "error: glew: %s\n", glewGetErrorString(err));
+ return 0;
+ }
+ printf("GLEW: %s\n", glewGetString(GLEW_VERSION));
+
+ glEnable(GL_TEXTURE_2D);
+ gl->screenshot_width = s->image->width;
+ gl->screenshot_height = s->image->height;
+
+ return 1;
+}
+
+static void opengl_create_program(OpenGLContext *gl) {
+ GLuint vertex = opengl_compile_shader(GL_VERTEX_SHADER, VERTEX_SHADER_SOURCE);
+ GLuint fragment = opengl_compile_shader(GL_FRAGMENT_SHADER, FRAGMENT_SHADER_SOURCE);
+
+ gl->program = glCreateProgram();
+ glAttachShader(gl->program, vertex);
+ glAttachShader(gl->program, fragment);
+ glLinkProgram(gl->program);
+
+ GLint success;
+ glGetProgramiv(gl->program, GL_LINK_STATUS, &success);
+ if (!success) {
+ char log[512];
+ glGetProgramInfoLog(gl->program, 512, NULL, log);
+ fprintf(stderr, "error: program link: %s\n", log);
+ }
+
+ glDeleteShader(vertex);
+ glDeleteShader(fragment);
+ glUseProgram(gl->program);
+ glUniform1i(glGetUniformLocation(gl->program, "tex"), 0);
+}
+
+static void opengl_create_texture(OpenGLContext *gl, App *app, Screenshot *s) {
+ glGenTextures(1, &gl->texture);
+ glActiveTexture(GL_TEXTURE0);
+ glBindTexture(GL_TEXTURE_2D, gl->texture);
+ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB,
+ s->image->width, s->image->height, 0,
+ GL_BGRA, GL_UNSIGNED_BYTE, s->image->data);
+
+ glGenerateMipmap(GL_TEXTURE_2D);
+ if (app->config.texture_filter) {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ } else {
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+ }
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
+}
+
+static void opengl_create_geometry(OpenGLContext *gl) {
+ float v[] = {
+ gl->screenshot_width, 0, 0.0f, 1.0f, 1.0f,
+ gl->screenshot_width, gl->screenshot_height, 0.0f, 1.0f, 0.0f,
+ 0, gl->screenshot_height, 0.0f, 0.0f, 0.0f,
+ 0, 0, 0.0f, 0.0f, 1.0f,
+ };
+
+ unsigned int i[] = {0, 1, 3, 1, 2, 3};
+
+ glGenVertexArrays(1, &gl->vao);
+ glGenBuffers(1, &gl->vbo);
+ glGenBuffers(1, &gl->ebo);
+
+ glBindVertexArray(gl->vao);
+ glBindBuffer(GL_ARRAY_BUFFER, gl->vbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(v), v, GL_STATIC_DRAW);
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gl->ebo);
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(i), i, GL_STATIC_DRAW);
+
+ glVertexAttribPointer(0, 3, GL_FLOAT,
+ GL_FALSE, 5 * sizeof(float), (void*)0);
+ glEnableVertexAttribArray(0);
+ glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE,
+ 5 * sizeof(float), (void*)(3 * sizeof(float)));
+ glEnableVertexAttribArray(1);
+}
+
+static void opengl_render(OpenGLContext *gl, App *app, int ww, int wh) {
+ glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
+
+ glUseProgram(gl->program);
+ glUniform2f(glGetUniformLocation(gl->program, "camera_pos"),
+ app->state.camera.position.x, app->state.camera.position.y);
+ glUniform1f(glGetUniformLocation(gl->program, "camera_scale"),
+ app->state.camera.scale);
+ glUniform2f(glGetUniformLocation(gl->program, "window_size"), ww, wh);
+ glUniform2f(glGetUniformLocation(gl->program, "screenshot_size"),
+ gl->screenshot_width, gl->screenshot_height);
+ glUniform2f(glGetUniformLocation(gl->program, "cursor_pos"),
+ app->state.mouse.curr.x, app->state.mouse.curr.y);
+ glUniform1f(glGetUniformLocation(gl->program, "fl_shadow"),
+ app->state.flashlight.shadow);
+ glUniform1f(glGetUniformLocation(gl->program, "fl_radius"),
+ app->state.flashlight.radius);
+ glUniform1f(glGetUniformLocation(gl->program, "fl_feather"),
+ app->config.feather);
+ glUniform1f(glGetUniformLocation(gl->program, "mirror"),
+ (float)app->state.mirror);
+
+ glBindTexture(GL_TEXTURE_2D, gl->texture);
+ glBindVertexArray(gl->vao);
+ glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
+}
+
+static void opengl_cleanup(OpenGLContext *gl) {
+ if (gl->vao) glDeleteVertexArrays(1, &gl->vao);
+ if (gl->vbo) glDeleteBuffers(1, &gl->vbo);
+ if (gl->ebo) glDeleteBuffers(1, &gl->ebo);
+ if (gl->program) glDeleteProgram(gl->program);
+ if (gl->texture) glDeleteTextures(1, &gl->texture);
+}
+
+// ================ main logic
+
+static void camera_update(App *app, Vec2f ws) {
+
+ Config *cfg = &app->config;
+ Camera *c = &app->state.camera;
+ Mouse *m = &app->state.mouse;
+ float dt = app->state.dt;
+
+ if (fabs(c->delta_scale) > app->config.scale_change_threshold) {
+ Vec2f half = vec2_mul(ws, 0.5f);
+ Vec2f sub = vec2_sub(c->scale_pivot, half);
+ Vec2f p0 = vec2_div(sub, c->scale);
+
+ c->scale += c->delta_scale * dt;
+ if (c->scale < cfg->min_scale) c->scale = cfg->min_scale;
+
+ Vec2f p1 = vec2_div(sub, c->scale);
+ c->position = vec2_add(c->position, vec2_sub(p0, p1));
+ c->delta_scale -= c->delta_scale * dt * cfg->scale_friction;
+ }
+
+ if (!m->drag && vec2_length(c->velocity) > cfg->velocity_threshold) {
+ c->position = vec2_add(c->position, vec2_mul(c->velocity, dt));
+ c->velocity = vec2_sub(c->velocity, vec2_mul(c->velocity, dt * cfg->drag_friction));
+ }
+}
+
+static void flashlight_update(App *app) {
+
+ Flashlight *fl = &app->state.flashlight;
+ float dt = app->state.dt;
+
+ fl->shadow = fl->enabled ?
+ fmin(fl->shadow + app->config.fade_speed * dt, app->config.max_shadow_opacity) :
+ fmax(fl->shadow - app->config.fade_speed * dt, 0.0f);
+
+ if (fabs(fl->delta_radius) > app->config.radius_change_threshold) {
+ fl->radius = fmax(0.0f, fl->radius + fl->delta_radius * dt);
+ fl->delta_radius -= fl->delta_radius * app->config.radius_damping * dt;
+ }
+}
+
+static Vec2f world_position(Camera *camera, Vec2f pos) {
+ return vec2_div(pos, camera->scale);
+}
+
+// ================ handlers
+
+static char *app_config_path; // stored config path for reload
+
+static void handle_keypress(XKeyEvent *ke, App *app, Mouse *m) {
+ KeySym key = XLookupKeysym(ke, 0);
+
+ if (key == app->config.key_escape || key == XK_q)
+ app->state.running = 0;
+
+ if (key == XK_r && app_config_path)
+ load_config_file(app_config_path, &app->config);
+
+ if (key == app->config.key_flashlight)
+ app->state.flashlight.enabled = !app->state.flashlight.enabled;
+
+ if (key == app->config.key_reset) {
+ app->state.camera = (Camera){ .scale = 1.0f };
+ app->state.flashlight.shadow = 0.0f;
+ app->state.flashlight.radius = app->config.initial_radius;
+ app->state.flashlight.delta_radius = 0.0f;
+ }
+
+ if (key == app->config.key_mirror)
+ app->state.mirror = !app->state.mirror;
+
+ if (key == app->config.key_zoom_in) {
+ app->state.camera.delta_scale += app->config.scroll_speed;
+ app->state.camera.scale_pivot = m->curr;
+ }
+
+ if (key == app->config.key_zoom_out) {
+ app->state.camera.delta_scale -= app->config.scroll_speed;
+ app->state.camera.scale_pivot = m->curr;
+ }
+}
+
+static void handle_mousemove(XMotionEvent *motion, App *app, int rr) {
+ app->state.mouse.curr = (Vec2f){ .x = motion->x, .y = motion->y };
+
+ if (app->state.mouse.drag) {
+ Vec2f prev = world_position(&app->state.camera, app->state.mouse.prev);
+ Vec2f cur = world_position(&app->state.camera, app->state.mouse.curr);
+ app->state.camera.position = vec2_add(app->state.camera.position, vec2_sub(prev, cur));
+ app->state.camera.velocity = vec2_mul(vec2_sub(prev, cur), rr);
+ }
+
+ app->state.mouse.prev = app->state.mouse.curr;
+}
+
+static void handle_buttonpress(XButtonEvent *be, App *app) {
+ int ctrl_pressed = (be->state & app->config.modifier_flashlight) != 0;
+
+ if (be->button == app->config.button_drag) {
+ app->state.mouse.prev = app->state.mouse.curr;
+ app->state.mouse.drag = 1;
+ app->state.camera.velocity = (Vec2f){ .x = 0, .y = 0 };
+ } else if (be->button == app->config.button_zoom_in) {
+ if (ctrl_pressed && app->state.flashlight.enabled) {
+ app->state.flashlight.delta_radius += app->config.initial_delta_radius;
+ } else {
+ app->state.camera.delta_scale += app->config.scroll_speed;
+ app->state.camera.scale_pivot = app->state.mouse.curr;
+ }
+ } else if (be->button == app->config.button_zoom_out) {
+ if (ctrl_pressed && app->state.flashlight.enabled) {
+ app->state.flashlight.delta_radius -= app->config.initial_delta_radius;
+ } else {
+ app->state.camera.delta_scale -= app->config.scroll_speed;
+ app->state.camera.scale_pivot = app->state.mouse.curr;
+ }
+ }
+}
+
+static void handle_buttonrelease(XButtonEvent *be, App *app) {
+ if (be->button == app->config.button_drag) {
+ app->state.mouse.drag = 0;
+ }
+}
+
+static void process_events(X11Context *x11, App *app) {
+ XEvent ev;
+ while (XPending(x11->display)) {
+ XNextEvent(x11->display, &ev);
+
+ switch (ev.type) {
+ case KeyPress:
+ handle_keypress(&ev.xkey, app, &app->state.mouse);
+ break;
+ case MotionNotify:
+ handle_mousemove(&ev.xmotion, app, x11->refresh_rate);
+ break;
+ case ButtonPress:
+ handle_buttonpress(&ev.xbutton, app);
+ break;
+ case ButtonRelease:
+ handle_buttonrelease(&ev.xbutton, app);
+ break;
+ case ClientMessage:
+ if ((Atom)ev.xclient.data.l[0] == x11->wm_delete_window)
+ app->state.running = 0;
+ break;
+ }
+ }
+}
+
+// ================ init
+
+static void init_app(App *app, const char *config_path) {
+ app->config = default_config;
+
+ if (config_path) {
+ app_config_path = strdup(config_path);
+ load_config_file(config_path, &app->config);
+ } else {
+ app_config_path = NULL;
+ }
+
+ app->state.camera = (Camera){ .scale = 1.0f };
+ app->state.mouse = (Mouse){0};
+ app->state.flashlight = (Flashlight){
+ .enabled = 0,
+ .shadow = 0.0f,
+ .radius = app->config.initial_radius,
+ .delta_radius = 0.0f
+ };
+ app->state.dt = 0.0f;
+ app->state.running = 1;
+ app->state.mirror = 0;
+}
+
+static void init_mouse_position(X11Context *x11, Mouse *m) {
+ Window root_return, child_return;
+ int root_x, root_y, win_x, win_y;
+ unsigned int mask;
+ XQueryPointer(x11->display, x11->root, &root_return, &child_return,
+ &root_x, &root_y, &win_x, &win_y, &mask);
+ m->curr = (Vec2f){ .x = win_x, .y = win_y };
+ m->prev = m->curr;
+}
+
+// ================ main loop
+
+static void main_loop(X11Context *x11, OpenGLContext *gl, App *app) {
+ app->state.dt = 1.0f / x11->refresh_rate;
+
+ while (app->state.running) {
+ x11_grab_focus(x11);
+
+ int ww, wh;
+ x11_get_window_size(x11, &ww, &wh);
+ glViewport(0, 0, ww, wh);
+
+ process_events(x11, app);
+
+ camera_update(app, vec2(ww, wh));
+ flashlight_update(app);
+
+ opengl_render(gl, app, ww, wh);
+
+ glXSwapBuffers(x11->display, x11->window);
+ glFinish();
+ }
+}
+
+// ================ entry point
+
+static void usage(void) {
+ printf("Usage: boomer [OPTIONS]\n"
+ " -d, --delay <seconds: float> delay execution of the program by provided <seconds>\n"
+ " -h, --help show this help and exit\n"
+ " --new-config [filepath] generate a new default config at [filepath]\n"
+ " -c, --config <filepath> use config at <filepath>\n"
+ " -V, --version show the current version and exit\n"
+ " -w, --windowed windowed mode instead of fullscreen\n");
+}
+
+int main(int argc, char **argv) {
+ int windowed = 0;
+ float delay_sec = 0.0f;
+ char *config_path = NULL;
+ char *new_cfg_out = NULL;
+
+ for (int i = 1; i < argc; i++) {
+ if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
+ usage();
+ return 0;
+ }
+ else if (strcmp(argv[i], "-V") == 0 || strcmp(argv[i], "--version") == 0) {
+ printf("boomer-%s\n", VERSION);
+ return 0;
+ }
+ else if (strcmp(argv[i], "-w") == 0 || strcmp(argv[i], "--windowed") == 0) {
+ windowed = 1;
+ }
+ else if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--delay") == 0) {
+ if (i + 1 >= argc) {
+ fprintf(stderr, "error: no value provided for %s\n", argv[i]);
+ usage();
+ return 1;
+ }
+ delay_sec = atof(argv[++i]);
+ }
+ else if (strcmp(argv[i], "-c") == 0 || strcmp(argv[i], "--config") == 0) {
+ if (i + 1 >= argc) {
+ fprintf(stderr, "error: no value provided for %s\n", argv[i]);
+ usage();
+ return 1;
+ }
+ config_path = argv[++i];
+ }
+ else if (strcmp(argv[i], "--new-config") == 0) {
+ new_cfg_out = get_default_config_path();
+ if (i + 1 < argc && argv[i + 1][0] != '-') {
+ new_cfg_out = argv[++i];
+ }
+ char *dir = strdup(new_cfg_out);
+ char *slash = strrchr(dir, '/');
+ if (slash) *slash = '\0';
+ mkdir_p(dir);
+ free(dir);
+ write_default_config(new_cfg_out);
+ printf("Generated config at %s\n", new_cfg_out);
+ return 0;
+ }
+ else {
+ fprintf(stderr, "error: unknown flag `%s`\n", argv[i]);
+ usage();
+ return 1;
+ }
+ }
+
+ if (delay_sec > 0.0f) {
+ long sec = (long)delay_sec;
+ long nsec = (long)((delay_sec - sec) * 1000000000.0f);
+ struct timespec ts = { .tv_sec = sec, .tv_nsec = nsec };
+ nanosleep(&ts, NULL);
+ }
+
+ if (!config_path) {
+ config_path = get_default_config_path();
+ }
+
+ printf("Using config: %s\n", config_path);
+
+ // init x11
+ X11Context x11 = {0};
+ if (!x11_init(&x11)) return 1;
+ if (!x11_check_glx(&x11)) { x11_cleanup(&x11); return 1; }
+ if (!x11_create_window(&x11, windowed)) { x11_cleanup(&x11); return 1; }
+
+ // make screenshot
+ Screenshot *screenshot = new_screenshot(x11.display, x11.root);
+ if (!screenshot || !screenshot->image) {
+ fprintf(stderr, "error: failed to take screenshot\n");
+ x11_cleanup(&x11);
+ return 1;
+ }
+ printf("Screenshot: %dx%d\n",
+ screenshot->image->width, screenshot->image->height);
+
+ // init opengl
+ OpenGLContext gl = {0};
+ if (!opengl_init(&gl, screenshot)) {
+ destroy_screenshot(x11.display, screenshot);
+ x11_cleanup(&x11);
+ return 1;
+ }
+
+ // init app
+ App app;
+ init_app(&app, config_path);
+ init_mouse_position(&x11, &app.state.mouse);
+
+ opengl_create_program(&gl);
+ opengl_create_texture(&gl, &app, screenshot);
+ opengl_create_geometry(&gl);
+
+ // main cycle
+ main_loop(&x11, &gl, &app);
+
+ // cleanup
+ x11_restore_focus(&x11);
+ opengl_cleanup(&gl);
+ destroy_screenshot(x11.display, screenshot);
+ x11_cleanup(&x11);
+ free(app_config_path);
+
+ return 0;
+}