diff options
| author | kj_sh604 | 2026-06-05 16:08:47 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-06-05 16:08:47 -0400 |
| commit | ffb0182d90d5607ccccff5210a2e711d6af35458 (patch) | |
| tree | 3bb0cee0f2917a66d735b1e08797da0dd9666f45 | |
| parent | 8c3af04bf55c334500252faca56fae61429fb770 (diff) | |
refactor: zig re-implementation
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | Makefile | 30 | ||||
| -rw-r--r-- | README | 13 | ||||
| -rw-r--r-- | build.zig | 30 | ||||
| -rw-r--r-- | src/app.zig | 208 | ||||
| -rw-r--r-- | src/c.zig | 15 | ||||
| -rw-r--r-- | src/config.h | 89 | ||||
| -rw-r--r-- | src/config.zig | 133 | ||||
| -rw-r--r-- | src/kj-boomer.c | 809 | ||||
| -rw-r--r-- | src/la.h | 45 | ||||
| -rw-r--r-- | src/main.zig | 178 | ||||
| -rw-r--r-- | src/math.zig | 32 | ||||
| -rw-r--r-- | src/opengl.zig | 283 | ||||
| -rw-r--r-- | src/screenshot.h | 96 | ||||
| -rw-r--r-- | src/screenshot.zig | 59 | ||||
| -rw-r--r-- | src/x11.zig | 145 | ||||
| -rw-r--r-- | static/demka.gif | bin | 1059927 -> 0 bytes |
17 files changed, 1105 insertions, 1065 deletions
@@ -3,4 +3,7 @@ boomer release TAGS -digest.txt
\ No newline at end of file +digest.txt +zig-out/ +zig-cache/ +.zig-cache/ @@ -1,35 +1,27 @@ -CC = cc -CFLAGS = -Wall -Wextra -O2 -std=c99 -LDFLAGS = -lX11 -lGL -lXext -lXrandr -lGLEW -lm TARGET = boomer +PREFIX ?= $(HOME)/.local -SRC_DIR = src - -# XShm support (disabled by default, enable with: make USE_XSHM=1) ifdef USE_XSHM -CFLAGS += -DUSE_XSHM +ZIG_XSHM_FLAG = -DUSE_XSHM=true endif -all: $(TARGET) - -$(TARGET): $(SRC_DIR)/kj-boomer.c $(SRC_DIR)/screenshot.h $(SRC_DIR)/la.h $(SRC_DIR)/config.h - $(CC) $(CFLAGS) -o $(TARGET) $(SRC_DIR)/kj-boomer.c $(LDFLAGS) +all: + zig build $(ZIG_XSHM_FLAG) + cp zig-out/bin/$(TARGET) ./$(TARGET) -release: $(TARGET) +release: all mkdir -p release - sharun lib4bin --with-wrappe --strip --dst-dir release ./$(TARGET) + sharun lib4bin --with-wrappe --strip --dst-dir release ./zig-out/bin/$(TARGET) clean: rm -f $(TARGET) - rm -rf release - -PREFIX ?= $(HOME)/.local + rm -rf zig-out zig-cache release -install: $(TARGET) +install: all mkdir -p $(PREFIX)/bin - install -Dm755 $(TARGET) $(PREFIX)/bin/$(TARGET) + install -Dm755 zig-out/bin/$(TARGET) $(PREFIX)/bin/$(TARGET) remove: rm -f $(PREFIX)/bin/$(TARGET) -.PHONY: all clean install remove +.PHONY: all clean install remove
\ No newline at end of file @@ -2,7 +2,7 @@ boomer ===== a zoomer application for linux. takes a screenshot and lets you pan, zoom, and -shine a flashlight around it. a C port of the original boomer by tsoding with no frills +shine a flashlight around it. a zig port of the original boomer by tsoding with no frills this is a personal re-implementation, so don't expect much. it works on my machine. @@ -31,12 +31,13 @@ left mouse pan the image scroll wheel zoom in/out ctrl+scroll change flashlight radius -you can change the controls in src/config.h and recompile. +you can change the controls in src/config.zig and recompile. dependencies ------------ +- zig (>= 0.16) - X11 development libraries (libX11, libXext, libXrandr) - OpenGL development libraries (libGL, libGLX) - GLEW (OpenGL Extension Wrangler) @@ -46,16 +47,16 @@ build ----- make -./boomer +./zig-out/bin/boomer for faster screenshot capture with MIT-SHM: make USE_XSHM=1 -./boomer +./zig-out/bin/boomer for a windowed mode instead of fullscreen: -./boomer -w +./zig-out/bin/boomer -w install @@ -71,4 +72,4 @@ make install PREFIX=/usr license ------- -0BSD +0BSD
\ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b46b525 --- /dev/null +++ b/build.zig @@ -0,0 +1,30 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const use_xshm = b.option(bool, "USE_XSHM", "enable mit-shm screenshot capture") orelse false; + + const options = b.addOptions(); + options.addOption(bool, "use_xshm", use_xshm); + + const exe = b.addExecutable(.{ + .name = "boomer", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + }); + + exe.root_module.addOptions("build_options", options); + + exe.root_module.linkSystemLibrary("X11", .{ .needed = true }); + exe.root_module.linkSystemLibrary("GL", .{ .needed = true }); + exe.root_module.linkSystemLibrary("GLEW", .{ .needed = true }); + exe.root_module.linkSystemLibrary("Xext", .{ .needed = true }); + exe.root_module.linkSystemLibrary("Xrandr", .{ .needed = true }); + + b.installArtifact(exe); +}
\ No newline at end of file diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..51d55c4 --- /dev/null +++ b/src/app.zig @@ -0,0 +1,208 @@ +const std = @import("std"); +const math = @import("math.zig"); +const config = @import("config.zig"); +const c = @import("c.zig").c; + +const Vec2f = math.Vec2f; +const Config = config.Config; + +pub const Camera = struct { + position: Vec2f = .{}, + velocity: Vec2f = .{}, + scale: f32 = 1.0, + delta_scale: f32 = 0.0, + scale_pivot: Vec2f = .{}, +}; + +pub const Mouse = struct { + curr: Vec2f = .{}, + prev: Vec2f = .{}, + drag: bool = false, +}; + +pub const Flashlight = struct { + enabled: bool = false, + shadow: f32 = 0.0, + radius: f32 = 0.0, + delta_radius: f32 = 0.0, +}; + +pub const State = struct { + camera: Camera = .{}, + mouse: Mouse = .{}, + flashlight: Flashlight = .{}, + dt: f32 = 0.0, + running: bool = true, + mirror: bool = false, +}; + +pub const App = struct { + config: Config = Config.default(), + state: State = .{}, + config_path: ?[]const u8 = null, + allocator: std.mem.Allocator, + + pub fn init(alloc: std.mem.Allocator, cfg_path: ?[]const u8) !App { + var app = App{ + .allocator = alloc, + .config = Config.default(), + }; + + if (cfg_path) |p| { + app.config_path = try alloc.dupe(u8, p); + app.config.loadFromFile(p); + } + + app.state.flashlight.radius = app.config.initial_radius; + + return app; + } + + pub fn deinit(self: *App) void { + if (self.config_path) |p| { + self.allocator.free(p); + } + } + + pub fn cameraUpdate(self: *App, ws: Vec2f) void { + const cfg = &self.config; + const cam = &self.state.camera; + const m = &self.state.mouse; + const dt = self.state.dt; + + if (@abs(cam.delta_scale) > cfg.scale_change_threshold) { + const half = ws.mul(0.5); + const sub = cam.scale_pivot.sub(half); + const p0 = sub.div(cam.scale); + + cam.scale += cam.delta_scale * dt; + if (cam.scale < cfg.min_scale) cam.scale = cfg.min_scale; + + const p1 = sub.div(cam.scale); + cam.position = cam.position.add(p0.sub(p1)); + cam.delta_scale -= cam.delta_scale * dt * cfg.scale_friction; + } + + if (!m.drag and cam.velocity.length() > cfg.velocity_threshold) { + cam.position = cam.position.add(cam.velocity.mul(dt)); + cam.velocity = cam.velocity.sub(cam.velocity.mul(dt * cfg.drag_friction)); + } + } + + pub fn flashlightUpdate(self: *App) void { + const fl = &self.state.flashlight; + const dt = self.state.dt; + const cfg = &self.config; + + fl.shadow = if (fl.enabled) + @min(fl.shadow + cfg.fade_speed * dt, cfg.max_shadow_opacity) + else + @max(fl.shadow - cfg.fade_speed * dt, 0.0); + + if (@abs(fl.delta_radius) > cfg.radius_change_threshold) { + fl.radius = @max(0.0, fl.radius + fl.delta_radius * dt); + fl.delta_radius -= fl.delta_radius * cfg.radius_damping * dt; + } + } + + fn worldPosition(camera: *Camera, pos: Vec2f) Vec2f { + return pos.div(camera.scale); + } + + pub fn processEvents(self: *App, x11: anytype) void { + var ev: c.XEvent = undefined; + while (c.XPending(x11.display) != 0) { + _ = c.XNextEvent(x11.display, &ev); + + switch (ev.type) { + c.KeyPress => self.handleKeypress(&ev.xkey), + c.MotionNotify => self.handleMousemove(&ev.xmotion, x11.refresh_rate), + c.ButtonPress => self.handleButtonpress(&ev.xbutton), + c.ButtonRelease => self.handleButtonrelease(&ev.xbutton), + c.ClientMessage => { + if (@as(c.Atom, @bitCast(ev.xclient.data.l[0])) == x11.wm_delete_window) + self.state.running = false; + }, + else => {}, + } + } + } + + fn handleKeypress(self: *App, ke: *c.XKeyEvent) void { + const key = c.XLookupKeysym(ke, 0); + + if (key == self.config.key_escape or key == c.XK_q) + self.state.running = false; + + if (key == c.XK_r) { + if (self.config_path) |p| { + self.config.loadFromFile(p); + } + } + + if (key == self.config.key_flashlight) + self.state.flashlight.enabled = !self.state.flashlight.enabled; + + if (key == self.config.key_reset) { + self.state.camera = .{ .scale = 1.0 }; + self.state.flashlight.shadow = 0.0; + self.state.flashlight.radius = self.config.initial_radius; + self.state.flashlight.delta_radius = 0.0; + } + + if (key == self.config.key_mirror) + self.state.mirror = !self.state.mirror; + + if (key == self.config.key_zoom_in) { + self.state.camera.delta_scale += self.config.scroll_speed; + self.state.camera.scale_pivot = self.state.mouse.curr; + } + + if (key == self.config.key_zoom_out) { + self.state.camera.delta_scale -= self.config.scroll_speed; + self.state.camera.scale_pivot = self.state.mouse.curr; + } + } + + fn handleMousemove(self: *App, motion: *c.XMotionEvent, rr: i32) void { + self.state.mouse.curr = .{ .x = @floatFromInt(motion.x), .y = @floatFromInt(motion.y) }; + + if (self.state.mouse.drag) { + const prev = worldPosition(&self.state.camera, self.state.mouse.prev); + const cur = worldPosition(&self.state.camera, self.state.mouse.curr); + self.state.camera.position = self.state.camera.position.add(prev.sub(cur)); + self.state.camera.velocity = prev.sub(cur).mul(@floatFromInt(rr)); + } + + self.state.mouse.prev = self.state.mouse.curr; + } + + fn handleButtonpress(self: *App, be: *c.XButtonEvent) void { + const ctrl_pressed = (be.state & self.config.modifier_flashlight) != 0; + + if (be.button == self.config.button_drag) { + self.state.mouse.prev = self.state.mouse.curr; + self.state.mouse.drag = true; + self.state.camera.velocity = .{}; + } else if (be.button == self.config.button_zoom_in) { + if (ctrl_pressed and self.state.flashlight.enabled) { + self.state.flashlight.delta_radius += self.config.initial_delta_radius; + } else { + self.state.camera.delta_scale += self.config.scroll_speed; + self.state.camera.scale_pivot = self.state.mouse.curr; + } + } else if (be.button == self.config.button_zoom_out) { + if (ctrl_pressed and self.state.flashlight.enabled) { + self.state.flashlight.delta_radius -= self.config.initial_delta_radius; + } else { + self.state.camera.delta_scale -= self.config.scroll_speed; + self.state.camera.scale_pivot = self.state.mouse.curr; + } + } + } + + fn handleButtonrelease(self: *App, be: *c.XButtonEvent) void { + if (be.button == self.config.button_drag) + self.state.mouse.drag = false; + } +};
\ No newline at end of file diff --git a/src/c.zig b/src/c.zig new file mode 100644 index 0000000..5d539d2 --- /dev/null +++ b/src/c.zig @@ -0,0 +1,15 @@ +const build_options = @import("build_options"); + +pub const c = @cImport({ + @cInclude("stdio.h"); + @cInclude("X11/Xlib.h"); + @cInclude("X11/Xutil.h"); + @cInclude("X11/keysym.h"); + @cInclude("X11/extensions/Xrandr.h"); + @cInclude("GL/glew.h"); + @cInclude("GL/glx.h"); + if (build_options.use_xshm) { + @cInclude("X11/extensions/XShm.h"); + @cInclude("sys/shm.h"); + } +});
\ No newline at end of file diff --git a/src/config.h b/src/config.h deleted file mode 100644 index 7dea51e..0000000 --- a/src/config.h +++ /dev/null @@ -1,89 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H - -#include <X11/keysym.h> - -typedef struct { - // camera settings - float min_scale; // minimum allowed zoom scale - float scroll_speed; // speed of zoom when scrolling or using +/- keys - float drag_friction; // friction coefficient for camera movement inertia - float scale_friction; // friction coefficient for zoom inertia - float velocity_threshold; // minimum velocity to apply inertia - float scale_change_threshold; // minimum magnitude to update camera zoom (skip minor changes) - - // flashlight settings - float initial_radius; // starting flashlight radius - float initial_delta_radius; // initial delta for flashlight radius change per Ctrl+scroll - float radius_damping; // radius attenuation coefficient - float fade_speed; // speed of flashlight fade in/out - float max_shadow_opacity; // maximum shadow opacity - float radius_change_threshold; // minimum magnitude to update flashlight radius (skip minor changes) - float feather; // soft edge size as percentage of radius (0.0 to 0.5, e.g., 0.15 = 15%) - - // opengl settings - int texture_filter; // 0 = pixelated, 1 = smooth - - // key bindings - KeySym key_escape; // key to quit the program - KeySym key_flashlight; // key to toggle flashlight - KeySym key_reset; // key to reset state - KeySym key_mirror; // key to toggle mirror - KeySym key_zoom_in; // key to zoom in - KeySym key_zoom_out; // key to zoom out - unsigned int modifier_flashlight; // modifier for flashlight radius change (e.g., ControlMask) - - // mouse bindings - unsigned int button_drag; // mouse button for dragging - unsigned int button_zoom_in; // mouse button for zoom in (scroll up) - unsigned int button_zoom_out; // mouse button for zoom out (scroll down) -} Config; - -#ifdef CONFIG_IMPL - -// you can hack these values -Config default_config = { - // camera settings - .min_scale = 0.5f, - .scroll_speed = 1.5f, - .drag_friction = 6.0f, - .scale_friction = 4.0f, - .velocity_threshold = 15.0f, - .scale_change_threshold = 0.5f, - - // flashlight settings - .initial_radius = 200.0f, - .initial_delta_radius = 250.0f, - .radius_damping = 10.0f, - .fade_speed = 6.0f, - .max_shadow_opacity = 0.8f, - .radius_change_threshold = 1.0f, - .feather = 0.0f, - - // opengl settings - .texture_filter = 0, - - // key bindings - .key_escape = XK_Escape, - .key_flashlight = XK_f, - .key_reset = XK_0, - .key_mirror = XK_m, - .key_zoom_in = XK_equal, - .key_zoom_out = XK_minus, - - // Ctrl = ControlMask, - // Left Alt = Mod1Mask, - // Shift = ShiftMask, - // Ctrl or Shift = ControlMask | ShiftMask, - // etc. - .modifier_flashlight = ControlMask, - - // mouse bindings - .button_drag = Button1, - .button_zoom_in = Button4, - .button_zoom_out = Button5, -}; - -#endif // CONFIG_IMPL - -#endif // CONFIG_H diff --git a/src/config.zig b/src/config.zig new file mode 100644 index 0000000..6325b0b --- /dev/null +++ b/src/config.zig @@ -0,0 +1,133 @@ +const std = @import("std"); + +const c = @import("c.zig").c; + +pub const Config = struct { + // camera settings + min_scale: f32 = 0.5, + scroll_speed: f32 = 1.5, + drag_friction: f32 = 6.0, + scale_friction: f32 = 4.0, + velocity_threshold: f32 = 15.0, + scale_change_threshold: f32 = 0.5, + + // flashlight settings + initial_radius: f32 = 200.0, + initial_delta_radius: f32 = 250.0, + radius_damping: f32 = 10.0, + fade_speed: f32 = 6.0, + max_shadow_opacity: f32 = 0.8, + radius_change_threshold: f32 = 1.0, + feather: f32 = 0.0, + + // opengl settings + texture_filter: i32 = 0, + + // key bindings (KeySym = c_ulong on x86_64 linux) + key_escape: c_ulong = c.XK_Escape, + key_flashlight: c_ulong = c.XK_f, + key_reset: c_ulong = c.XK_0, + key_mirror: c_ulong = c.XK_m, + key_zoom_in: c_ulong = c.XK_equal, + key_zoom_out: c_ulong = c.XK_minus, + + modifier_flashlight: c_uint = c.ControlMask, + button_drag: c_uint = c.Button1, + button_zoom_in: c_uint = c.Button4, + button_zoom_out: c_uint = c.Button5, + + pub fn default() Config { + return .{}; + } + + pub fn applyValue(self: *Config, key: []const u8, value: f32) void { + if (std.mem.eql(u8, key, "min_scale")) self.min_scale = value; + if (std.mem.eql(u8, key, "scroll_speed")) self.scroll_speed = value; + if (std.mem.eql(u8, key, "drag_friction")) self.drag_friction = value; + if (std.mem.eql(u8, key, "scale_friction")) self.scale_friction = value; + if (std.mem.eql(u8, key, "velocity_threshold")) self.velocity_threshold = value; + if (std.mem.eql(u8, key, "scale_change_threshold")) self.scale_change_threshold = value; + if (std.mem.eql(u8, key, "initial_radius")) self.initial_radius = value; + if (std.mem.eql(u8, key, "initial_delta_radius")) self.initial_delta_radius = value; + if (std.mem.eql(u8, key, "radius_damping")) self.radius_damping = value; + if (std.mem.eql(u8, key, "fade_speed")) self.fade_speed = value; + if (std.mem.eql(u8, key, "max_shadow_opacity")) self.max_shadow_opacity = value; + if (std.mem.eql(u8, key, "radius_change_threshold")) self.radius_change_threshold = value; + if (std.mem.eql(u8, key, "feather")) self.feather = value; + if (std.mem.eql(u8, key, "texture_filter")) self.texture_filter = @intFromFloat(value); + } + + pub fn loadFromFile(self: *Config, path: []const u8) void { + var path_buf: [1024]u8 = undefined; + @memcpy(path_buf[0..@min(path.len, path_buf.len - 1)], path); + path_buf[@min(path.len, path_buf.len - 1)] = 0; + const path_z: [*:0]const u8 = @ptrCast(&path_buf); + + const f = c.fopen(path_z, "r") orelse return; + defer _ = c.fclose(f); + + var buf: [4096]u8 = undefined; + while (c.fgets(&buf, buf.len, f) != null) { + const raw = buf[0 .. std.mem.indexOfScalar(u8, &buf, 0) orelse buf.len]; + const line = std.mem.trimStart(u8, raw, " \t"); + if (line.len == 0 or line[0] == '#') continue; + + var parts = std.mem.splitScalar(u8, line, '='); + const key = std.mem.trimEnd(u8, parts.first(), " \t"); + const val_str = if (parts.next()) |v| std.mem.trimStart(u8, v, " \t") else continue; + const value = std.fmt.parseFloat(f32, val_str) catch continue; + + self.applyValue(key, value); + } + } + + pub fn writeDefault(self: Config, path: []const u8) !void { + var path_buf: [1024]u8 = undefined; + @memcpy(path_buf[0..@min(path.len, path_buf.len - 1)], path); + path_buf[@min(path.len, path_buf.len - 1)] = 0; + const path_z: [*:0]const u8 = @ptrCast(&path_buf); + + const f = c.fopen(path_z, "w") orelse { + std.debug.print("error: could not write config to {s}\n", .{path}); + return error.ConfigWriteFailed; + }; + defer _ = c.fclose(f); + + _ = c.fprintf(f, "min_scale = %.1f\n", self.min_scale); + _ = c.fprintf(f, "scroll_speed = %.1f\n", self.scroll_speed); + _ = c.fprintf(f, "drag_friction = %.1f\n", self.drag_friction); + _ = c.fprintf(f, "scale_friction = %.1f\n", self.scale_friction); + _ = c.fprintf(f, "velocity_threshold = %.1f\n", self.velocity_threshold); + _ = c.fprintf(f, "scale_change_threshold = %.2f\n", self.scale_change_threshold); + _ = c.fprintf(f, "initial_radius = %.1f\n", self.initial_radius); + _ = c.fprintf(f, "initial_delta_radius = %.1f\n", self.initial_delta_radius); + _ = c.fprintf(f, "radius_damping = %.1f\n", self.radius_damping); + _ = c.fprintf(f, "fade_speed = %.1f\n", self.fade_speed); + _ = c.fprintf(f, "max_shadow_opacity = %.2f\n", self.max_shadow_opacity); + _ = c.fprintf(f, "radius_change_threshold = %.2f\n", self.radius_change_threshold); + _ = c.fprintf(f, "feather = %.2f\n", self.feather); + _ = c.fprintf(f, "texture_filter = %d\n", self.texture_filter); + } + + pub fn defaultConfigPath(alloc: std.mem.Allocator) ?[]const u8 { + const home = std.c.getenv("HOME") orelse return null; + const home_slice = std.mem.sliceTo(home, 0); + const path = std.fs.path.join(alloc, &.{ home_slice, ".config", "boomer", "config" }) catch return null; + return path; + } + + pub fn mkdirP(path: []const u8) void { + var buf: [1024]u8 = undefined; + const len = @min(path.len, buf.len - 1); + @memcpy(buf[0..len], path[0..len]); + buf[len] = 0; + for (1..len) |i| { + if (buf[i] == '/') { + buf[i] = 0; + _ = std.c.mkdir((buf[0..i :0]).ptr, 0o755); + buf[i] = '/'; + } + } + _ = std.c.mkdir((buf[0..len :0]).ptr, 0o755); + } +};
\ No newline at end of file diff --git a/src/kj-boomer.c b/src/kj-boomer.c deleted file mode 100644 index af4472a..0000000 --- a/src/kj-boomer.c +++ /dev/null @@ -1,809 +0,0 @@ -// 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; -} diff --git a/src/la.h b/src/la.h deleted file mode 100644 index af8a293..0000000 --- a/src/la.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef LA_H -#define LA_H - -#include <math.h> - -typedef struct { - float x, y; -} Vec2f; - -Vec2f vec2(float x, float y); -Vec2f vec2_add(Vec2f a, Vec2f b); -Vec2f vec2_sub(Vec2f a, Vec2f b); -Vec2f vec2_mul(Vec2f a, float s); -Vec2f vec2_div(Vec2f a, float s); -float vec2_length(Vec2f a); - -#ifdef LA_IMPL - -Vec2f vec2(float x, float y) { - return (Vec2f){x, y}; -} - -Vec2f vec2_add(Vec2f a, Vec2f b) { - return vec2(a.x + b.x, a.y + b.y); -} - -Vec2f vec2_sub(Vec2f a, Vec2f b) { - return vec2(a.x - b.x, a.y - b.y); -} - -Vec2f vec2_mul(Vec2f a, float s) { - return vec2(a.x * s, a.y * s); -} - -Vec2f vec2_div(Vec2f a, float s) { - return vec2(a.x / s, a.y / s); -} - -float vec2_length(Vec2f a) { - return sqrtf(a.x * a.x + a.y * a.y); -} - -#endif // LA_IMPL - -#endif // LA_H diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..2f6c56c --- /dev/null +++ b/src/main.zig @@ -0,0 +1,178 @@ +const std = @import("std"); +const config = @import("config.zig"); +const screenshot = @import("screenshot.zig"); +const x11 = @import("x11.zig"); +const opengl = @import("opengl.zig"); +const app = @import("app.zig"); +const math = @import("math.zig"); +const c = @import("c.zig").c; + +const VERSION = "20260426"; + +const usage_text = + \\usage: boomer [OPTIONS] + \\ -d, --delay <seconds: float> delay execution of the program by provided <seconds> + \\ -h, --help show this help and exit + \\ --new-config [filepath] generate a new default config at [filepath] + \\ -c, --config <filepath> use config at <filepath> + \\ -V, --version show the current version and exit + \\ -w, --windowed windowed mode instead of fullscreen +; + +pub fn main(init: std.process.Init) !void { + // cli args + var windowed: bool = false; + var delay_sec: f32 = 0.0; + var config_path: ?[]const u8 = null; + var new_cfg_out: ?[]const u8 = null; + + const argv = init.minimal.args.vector; + + var i: usize = 1; + while (i < argv.len) : (i += 1) { + const arg = std.mem.sliceTo(argv[i], 0); + if (std.mem.eql(u8, arg, "-h") or std.mem.eql(u8, arg, "--help")) { + std.debug.print("{s}", .{usage_text}); + return; + } else if (std.mem.eql(u8, arg, "-V") or std.mem.eql(u8, arg, "--version")) { + std.debug.print("boomer-{s}\n", .{VERSION}); + return; + } else if (std.mem.eql(u8, arg, "-w") or std.mem.eql(u8, arg, "--windowed")) { + windowed = true; + } else if (std.mem.eql(u8, arg, "-d") or std.mem.eql(u8, arg, "--delay")) { + i += 1; + if (i >= argv.len) { + std.debug.print("error: no value provided for {s}\n", .{arg}); + std.debug.print("{s}", .{usage_text}); + return error.InvalidArgs; + } + delay_sec = std.fmt.parseFloat(f32, std.mem.sliceTo(argv[i], 0)) catch { + std.debug.print("error: invalid delay value: {s}\n", .{std.mem.sliceTo(argv[i], 0)}); + return error.InvalidArgs; + }; + } else if (std.mem.eql(u8, arg, "-c") or std.mem.eql(u8, arg, "--config")) { + i += 1; + if (i >= argv.len) { + std.debug.print("error: no value provided for {s}\n", .{arg}); + std.debug.print("{s}", .{usage_text}); + return error.InvalidArgs; + } + config_path = std.mem.sliceTo(argv[i], 0); + } else if (std.mem.eql(u8, arg, "--new-config")) { + const alloc_path: ?[]const u8 = config.Config.defaultConfigPath(init.gpa); + defer if (alloc_path) |p| init.gpa.free(p); + if (i + 1 < argv.len and std.mem.sliceTo(argv[i + 1], 0)[0] != '-') { + i += 1; + new_cfg_out = std.mem.sliceTo(argv[i], 0); + } else { + new_cfg_out = alloc_path; + } + if (new_cfg_out) |p| { + const dir = std.fs.path.dirname(p) orelse "."; + config.Config.mkdirP(dir); + config.Config.default().writeDefault(p) catch { + std.debug.print("error: could not write config to {s}\n", .{p}); + return error.ConfigWriteFailed; + }; + std.debug.print("generated config at {s}\n", .{p}); + } + return; + } else { + std.debug.print("error: unknown flag `{s}`\n", .{arg}); + std.debug.print("{s}", .{usage_text}); + return error.InvalidArgs; + } + } + + // delay + if (delay_sec > 0.0) { + const ns: i64 = @intFromFloat(delay_sec * 1_000_000_000.0); + var ts = std.os.linux.timespec{ + .sec = @divFloor(ns, 1_000_000_000), + .nsec = @mod(ns, 1_000_000_000), + }; + _ = std.os.linux.nanosleep(&ts, null); + } + + // default config path + var alloc_config_path: ?[]const u8 = null; + if (config_path == null) { + alloc_config_path = config.Config.defaultConfigPath(init.gpa); + config_path = alloc_config_path; + } + defer if (alloc_config_path) |p| init.gpa.free(p); + + if (config_path) |p| { + std.debug.print("using config: {s}\n", .{p}); + } + + // init x11 + var xc = try x11.X11.init(); + defer xc.deinit(); + + try xc.checkGlx(); + try xc.createWindow(windowed); + + // screenshot + var ss = screenshot.Screenshot.capture(xc.display.?, xc.root) catch { + std.debug.print("error: failed to take screenshot\n", .{}); + return error.ScreenshotFailed; + }; + defer ss.deinit(); + + std.debug.print("screenshot: {d}x{d}\n", .{ ss.image.*.width, ss.image.*.height }); + + // init opengl + var gl = opengl.GL.init(&ss) catch { + return error.OpenGLInitFailed; + }; + defer gl.deinit(); + + // init app + var a = app.App.init(init.gpa, config_path) catch { + return error.AppInitFailed; + }; + defer a.deinit(); + + // seed mouse position + { + var root_ret: c.Window = undefined; + var child_ret: c.Window = undefined; + var rx: i32 = undefined; + var ry: i32 = undefined; + var wx: i32 = undefined; + var wy: i32 = undefined; + var mask: c_uint = undefined; + _ = c.XQueryPointer(xc.display, xc.root, &root_ret, &child_ret, &rx, &ry, &wx, &wy, &mask); + a.state.mouse.curr = .{ .x = @floatFromInt(wx), .y = @floatFromInt(wy) }; + a.state.mouse.prev = a.state.mouse.curr; + } + + gl.createProgram(); + gl.createTexture(&ss, a.config.texture_filter); + gl.createGeometry(); + + // main loop + a.state.dt = 1.0 / @as(f32, @floatFromInt(xc.refresh_rate)); + + while (a.state.running) { + xc.grabFocus(); + + var ww: i32 = undefined; + var wh: i32 = undefined; + xc.getWindowSize(&ww, &wh); + c.glViewport(0, 0, ww, wh); + + a.processEvents(&xc); + + a.cameraUpdate(math.Vec2f.init(@floatFromInt(ww), @floatFromInt(wh))); + a.flashlightUpdate(); + + gl.render(&a, ww, wh); + + c.glXSwapBuffers(xc.display, xc.window); + c.glFinish(); + } + + xc.restoreFocus(); +}
\ No newline at end of file diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..a9e9e23 --- /dev/null +++ b/src/math.zig @@ -0,0 +1,32 @@ +pub const Vec2f = struct { + x: f32 = 0, + y: f32 = 0, + + pub fn init(x: f32, y: f32) Vec2f { + return .{ .x = x, .y = y }; + } + + pub fn add(self: Vec2f, other: Vec2f) Vec2f { + return .{ .x = self.x + other.x, .y = self.y + other.y }; + } + + pub fn sub(self: Vec2f, other: Vec2f) Vec2f { + return .{ .x = self.x - other.x, .y = self.y - other.y }; + } + + pub fn mul(self: Vec2f, s: f32) Vec2f { + return .{ .x = self.x * s, .y = self.y * s }; + } + + pub fn div(self: Vec2f, s: f32) Vec2f { + return .{ .x = self.x / s, .y = self.y / s }; + } + + pub fn length(self: Vec2f) f32 { + return @sqrt(self.x * self.x + self.y * self.y); + } + + pub fn abs(self: Vec2f) Vec2f { + return .{ .x = @abs(self.x), .y = @abs(self.y) }; + } +};
\ No newline at end of file diff --git a/src/opengl.zig b/src/opengl.zig new file mode 100644 index 0000000..8402c62 --- /dev/null +++ b/src/opengl.zig @@ -0,0 +1,283 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +// opengl constants +const GL_COLOR_BUFFER_BIT = 0x00004000; +const GL_DEPTH_BUFFER_BIT = 0x00000100; +const GL_TEXTURE_2D = 0x0DE1; +const GL_RGB = 0x1907; +const GL_BGRA = 0x80E1; +const GL_UNSIGNED_BYTE = 0x1401; +const GL_NEAREST = 0x2600; +const GL_LINEAR = 0x2601; +const GL_CLAMP_TO_BORDER = 0x812D; +const GL_TEXTURE_MIN_FILTER = 0x2801; +const GL_TEXTURE_MAG_FILTER = 0x2800; +const GL_TEXTURE_WRAP_S = 0x2802; +const GL_TEXTURE_WRAP_T = 0x2803; +const GL_ARRAY_BUFFER = 0x8892; +const GL_ELEMENT_ARRAY_BUFFER = 0x8893; +const GL_STATIC_DRAW = 0x88E4; +const GL_FLOAT = 0x1406; +const GL_VERTEX_SHADER = 0x8B31; +const GL_FRAGMENT_SHADER = 0x8B30; +const GL_COMPILE_STATUS = 0x8B81; +const GL_LINK_STATUS = 0x8B82; +const GL_TEXTURE0 = 0x84C0; +const GL_TRIANGLES = 0x0004; +const GL_UNSIGNED_INT = 0x1405; +const GL_TRUE = 1; + +extern fn glewInit() c_uint; +extern fn glewGetErrorString(err: c_uint) [*c]const u8; +extern fn glewGetString(name: c_uint) [*c]const u8; + +extern fn glEnable(cap: c_uint) void; +extern fn glClearColor(r: f32, g: f32, b: f32, a: f32) void; +extern fn glClear(mask: c_uint) void; +extern fn glViewport(x: i32, y: i32, w: i32, h: i32) void; +extern fn glFinish() void; + +extern fn glCreateShader(type_: c_uint) c_uint; +extern fn glShaderSource(shader: c_uint, count: i32, string: [*c]const [*c]const u8, length: [*c]const i32) void; +extern fn glCompileShader(shader: c_uint) void; +extern fn glGetShaderiv(shader: c_uint, pname: c_uint, params: [*c]i32) void; +extern fn glGetShaderInfoLog(shader: c_uint, buf_size: i32, length: [*c]i32, info_log: [*c]u8) void; +extern fn glDeleteShader(shader: c_uint) void; + +extern fn glCreateProgram() c_uint; +extern fn glAttachShader(program: c_uint, shader: c_uint) void; +extern fn glLinkProgram(program: c_uint) void; +extern fn glGetProgramiv(program: c_uint, pname: c_uint, params: [*c]i32) void; +extern fn glGetProgramInfoLog(program: c_uint, buf_size: i32, length: [*c]i32, info_log: [*c]u8) void; +extern fn glDeleteProgram(program: c_uint) void; +extern fn glUseProgram(program: c_uint) void; + +extern fn glGetUniformLocation(program: c_uint, name: [*c]const u8) i32; +extern fn glUniform1i(location: i32, v0: i32) void; +extern fn glUniform1f(location: i32, v0: f32) void; +extern fn glUniform2f(location: i32, v0: f32, v1: f32) void; + +extern fn glGenTextures(n: i32, textures: [*c]c_uint) void; +extern fn glActiveTexture(texture: c_uint) void; +extern fn glBindTexture(target: c_uint, texture: c_uint) void; +extern fn glTexImage2D(target: c_uint, level: i32, internalformat: i32, width: i32, height: i32, border: i32, format: c_uint, type_: c_uint, pixels: ?*const anyopaque) void; +extern fn glGenerateMipmap(target: c_uint) void; +extern fn glTexParameteri(target: c_uint, pname: c_uint, param: i32) void; +extern fn glDeleteTextures(n: i32, textures: [*c]const c_uint) void; + +extern fn glGenVertexArrays(n: i32, arrays: [*c]c_uint) void; +extern fn glGenBuffers(n: i32, buffers: [*c]c_uint) void; +extern fn glBindVertexArray(array: c_uint) void; +extern fn glBindBuffer(target: c_uint, buffer: c_uint) void; +extern fn glBufferData(target: c_uint, size: isize, data: ?*const anyopaque, usage: c_uint) void; +extern fn glVertexAttribPointer(index: c_uint, size: i32, type_: c_uint, normalized: u8, stride: i32, pointer: ?*const anyopaque) void; +extern fn glEnableVertexAttribArray(index: c_uint) void; +extern fn glDrawElements(mode: c_uint, count: i32, type_: c_uint, indices: ?*const anyopaque) void; +extern fn glDeleteVertexArrays(n: i32, arrays: [*c]const c_uint) void; +extern fn glDeleteBuffers(n: i32, buffers: [*c]const c_uint) void; + +const vertex_shader_source = + \\#version 130 + \\in vec3 aPos; + \\in vec2 aTexCoord; + \\out vec2 texcoord; + \\uniform vec2 camera_pos; + \\uniform float camera_scale; + \\uniform vec2 window_size; + \\uniform vec2 screenshot_size; + \\vec3 to_world(vec3 v) { + \\ vec2 ratio = vec2( + \\ window_size.x / screenshot_size.x / camera_scale, + \\ window_size.y / screenshot_size.y / camera_scale); + \\ return vec3((v.x / screenshot_size.x * 2.0 - 1.0) / ratio.x, + \\ (v.y / screenshot_size.y * 2.0 - 1.0) / ratio.y, + \\ v.z); + \\} + \\void main() { + \\ gl_Position = vec4(to_world((aPos - vec3(camera_pos * vec2(1.0, -1.0), 0.0))), 1.0); + \\ texcoord = aTexCoord; + \\} +; + +const fragment_shader_source = + \\#version 130 + \\out mediump vec4 color; + \\in mediump vec2 texcoord; + \\uniform sampler2D tex; + \\uniform vec2 cursor_pos; + \\uniform vec2 window_size; + \\uniform float fl_shadow; + \\uniform float fl_radius; + \\uniform float camera_scale; + \\uniform float fl_feather; + \\uniform float mirror; + \\void main() { + \\ vec4 cursor = vec4(cursor_pos.x, window_size.y - cursor_pos.y, 0.0, 1.0); + \\ float dist = length(cursor - gl_FragCoord); + \\ float radius_px = fl_radius * camera_scale; + \\ float inner = radius_px * (1.0 - fl_feather); + \\ float outer = radius_px; + \\ float alpha = smoothstep(inner, outer, dist); + \\ vec2 tc = texcoord; + \\ if (mirror > 0.5) tc.x = 1.0 - tc.x; + \\ color = mix(texture(tex, tc), vec4(0.0, 0.0, 0.0, 0.0), alpha * fl_shadow); + \\} +; + +pub const GL = struct { + program: c_uint = 0, + texture: c_uint = 0, + vao: c_uint = 0, + vbo: c_uint = 0, + ebo: c_uint = 0, + screenshot_w: i32 = 0, + screenshot_h: i32 = 0, + + pub fn init(screenshot: anytype) !GL { + const err = glewInit(); + if (err != c.GLEW_OK) { + std.debug.print("error: glew: {s}\n", .{glewGetErrorString(err)}); + return error.GlewInitFailed; + } + std.debug.print("glew: {s}\n", .{glewGetString(c.GLEW_VERSION)}); + + glEnable(GL_TEXTURE_2D); + + return .{ + .screenshot_w = screenshot.image.*.width, + .screenshot_h = screenshot.image.*.height, + }; + } + + pub fn createProgram(self: *GL) void { + const vertex = compileShader(GL_VERTEX_SHADER, vertex_shader_source); + const fragment = compileShader(GL_FRAGMENT_SHADER, fragment_shader_source); + + self.program = glCreateProgram(); + glAttachShader(self.program, vertex); + glAttachShader(self.program, fragment); + glLinkProgram(self.program); + + var success: i32 = 0; + glGetProgramiv(self.program, GL_LINK_STATUS, &success); + if (success == 0) { + var log: [512]u8 = undefined; + glGetProgramInfoLog(self.program, 512, null, &log); + std.debug.print("error: program link: {s}\n", .{log}); + return; + } + + glDeleteShader(vertex); + glDeleteShader(fragment); + glUseProgram(self.program); + glUniform1i(glGetUniformLocation(self.program, "tex"), 0); + } + + pub fn createTexture(self: *GL, screenshot: anytype, filter: i32) void { + glGenTextures(1, &self.texture); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, self.texture); + glTexImage2D( + GL_TEXTURE_2D, 0, GL_RGB, + screenshot.image.*.width, screenshot.image.*.height, 0, + GL_BGRA, GL_UNSIGNED_BYTE, @ptrCast(screenshot.image.*.data), + ); + + glGenerateMipmap(GL_TEXTURE_2D); + if (filter != 0) { + 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); + } + + pub fn createGeometry(self: *GL) void { + const vertices = [_]f32{ + @floatFromInt(self.screenshot_w), 0, 0.0, 1.0, 1.0, + @floatFromInt(self.screenshot_w), @floatFromInt(self.screenshot_h), 0.0, 1.0, 0.0, + 0, @floatFromInt(self.screenshot_h), 0.0, 0.0, 0.0, + 0, 0, 0.0, 0.0, 1.0, + }; + + const indices = [_]c_uint{ 0, 1, 3, 1, 2, 3 }; + + glGenVertexArrays(1, &self.vao); + glGenBuffers(1, &self.vbo); + glGenBuffers(1, &self.ebo); + + glBindVertexArray(self.vao); + glBindBuffer(GL_ARRAY_BUFFER, self.vbo); + glBufferData(GL_ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, GL_STATIC_DRAW); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self.ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, GL_STATIC_DRAW); + + glVertexAttribPointer(0, 3, GL_FLOAT, 0, 5 * @sizeOf(f32), null); + glEnableVertexAttribArray(0); + glVertexAttribPointer(1, 2, GL_FLOAT, 0, 5 * @sizeOf(f32), @ptrFromInt(3 * @sizeOf(f32))); + glEnableVertexAttribArray(1); + } + + pub fn render(self: *GL, app: anytype, ww: i32, wh: i32) void { + glClearColor(0.1, 0.1, 0.1, 1.0); + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + glUseProgram(self.program); + + const cam = &app.state.camera; + const fl = &app.state.flashlight; + const mouse = &app.state.mouse; + + glUniform2f( + glGetUniformLocation(self.program, "camera_pos"), + cam.position.x, cam.position.y, + ); + glUniform1f(glGetUniformLocation(self.program, "camera_scale"), cam.scale); + glUniform2f(glGetUniformLocation(self.program, "window_size"), @floatFromInt(ww), @floatFromInt(wh)); + glUniform2f( + glGetUniformLocation(self.program, "screenshot_size"), + @floatFromInt(self.screenshot_w), @floatFromInt(self.screenshot_h), + ); + glUniform2f( + glGetUniformLocation(self.program, "cursor_pos"), + mouse.curr.x, mouse.curr.y, + ); + glUniform1f(glGetUniformLocation(self.program, "fl_shadow"), fl.shadow); + glUniform1f(glGetUniformLocation(self.program, "fl_radius"), fl.radius); + glUniform1f(glGetUniformLocation(self.program, "fl_feather"), app.config.feather); + glUniform1f(glGetUniformLocation(self.program, "mirror"), @floatFromInt(@intFromBool(app.state.mirror))); + + glBindTexture(GL_TEXTURE_2D, self.texture); + glBindVertexArray(self.vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, null); + } + + pub fn deinit(self: *GL) void { + if (self.vao != 0) glDeleteVertexArrays(1, &self.vao); + if (self.vbo != 0) glDeleteBuffers(1, &self.vbo); + if (self.ebo != 0) glDeleteBuffers(1, &self.ebo); + if (self.program != 0) glDeleteProgram(self.program); + if (self.texture != 0) glDeleteTextures(1, &self.texture); + } +}; + +fn compileShader(type_: c_uint, source: [:0]const u8) c_uint { + const shader = glCreateShader(type_); + const src_ptr: [*c]const u8 = source.ptr; + const src_len: i32 = @intCast(source.len); + glShaderSource(shader, 1, &src_ptr, &src_len); + glCompileShader(shader); + + var success: i32 = 0; + glGetShaderiv(shader, GL_COMPILE_STATUS, &success); + if (success == 0) { + var log: [512]u8 = undefined; + glGetShaderInfoLog(shader, 512, null, &log); + std.debug.print("error: shader compile: {s}\n", .{log}); + } + return shader; +}
\ No newline at end of file diff --git a/src/screenshot.h b/src/screenshot.h deleted file mode 100644 index 96cee42..0000000 --- a/src/screenshot.h +++ /dev/null @@ -1,96 +0,0 @@ -#ifndef SCREENSHOT_H -#define SCREENSHOT_H - -#include <X11/Xlib.h> -#include <X11/Xutil.h> - -#ifdef USE_XSHM -#include <X11/extensions/XShm.h> -#endif - -typedef struct { - XImage *image; -#ifdef USE_XSHM - XShmSegmentInfo *shminfo; -#endif -} Screenshot; - -Screenshot *new_screenshot(Display *display, Window window); -void destroy_screenshot(Display *display, Screenshot *screenshot); - -// TODO(20260315T135543): maybe add error checking - -#ifdef SCREENSHOT_IMPL - -#include <stdlib.h> - -#ifdef USE_XSHM -#include <sys/shm.h> -#endif - -#define UNUSED(x) (void)(x) - -Screenshot *new_screenshot(Display *d, Window w) { - Screenshot *result = malloc(sizeof(Screenshot)); - - XWindowAttributes attributes; - XGetWindowAttributes(d, w, &attributes); - -#ifdef USE_XSHM - result->shminfo = malloc(sizeof(XShmSegmentInfo)); - - int screen = DefaultScreen(d); - - result->image = XShmCreateImage( - d, - DefaultVisual(d, screen), - DefaultDepthOfScreen(ScreenOfDisplay(d, screen)), - ZPixmap, - NULL, - result->shminfo, - attributes.width, - attributes.height - ); - - result->shminfo->shmid = shmget( - IPC_PRIVATE, - result->image->bytes_per_line * result->image->height, - IPC_CREAT | 0777 - ); - - result->shminfo->shmaddr = (char*)shmat(result->shminfo->shmid, 0, 0); - result->image->data = result->shminfo->shmaddr; - result->shminfo->readOnly = False; - - XShmAttach(d, result->shminfo); - - XShmGetImage(d, w, result->image, 0, 0, AllPlanes); -#else // USE_XSHM - result->image = XGetImage(d, w, 0, 0, - attributes.width, attributes.height, - AllPlanes, ZPixmap); -#endif // USE_XSHM - - return result; -} - -void destroy_screenshot(Display *d, Screenshot *s) { - if (!s) return; - -#ifdef USE_XSHM - XSync(d, False); - XShmDetach(d, s->shminfo); - XDestroyImage(s->image); - shmdt(s->shminfo->shmaddr); - shmctl(s->shminfo->shmid, IPC_RMID, 0); - free(s->shminfo); -#else - UNUSED(d); - XDestroyImage(s->image); -#endif - free(s); -} - -#endif // SCREENSHOT_IMPL - -#endif // SCREENSHOT_H diff --git a/src/screenshot.zig b/src/screenshot.zig new file mode 100644 index 0000000..6f74429 --- /dev/null +++ b/src/screenshot.zig @@ -0,0 +1,59 @@ +const build_options = @import("build_options"); +const c = @import("c.zig").c; + +const std = @import("std"); + +extern fn XDestroyImage(ximage: *c.XImage) c_int; + +const ShmInfo = if (build_options.use_xshm) c.XShmSegmentInfo else struct {}; + +pub const Screenshot = struct { + image: *c.XImage, + shm: ShmInfo = .{}, + + pub fn capture(display: *c.Display, window: c.Window) !Screenshot { + var attrs: c.XWindowAttributes = undefined; + _ = c.XGetWindowAttributes(display, window, &attrs); + + if (build_options.use_xshm) { + const screen = c.XDefaultScreen(display); + + var ss = Screenshot{ .image = undefined, .shm = undefined }; + + ss.image = c.XShmCreateImage( + display, + c.XDefaultVisual(display, @intCast(screen)), + @intCast(c.XDefaultDepthOfScreen(c.XScreenOfDisplay(display, @intCast(screen)))), + c.ZPixmap, + null, + &ss.shm, + @intCast(attrs.width), + @intCast(attrs.height), + ) orelse return error.ScreenshotFailed; + + ss.shm.shmid = c.shmget(c.IPC_PRIVATE, @intCast(ss.image.*.bytes_per_line * ss.image.*.height), c.IPC_CREAT | 0o777); + ss.shm.shmaddr = @ptrCast(c.shmat(ss.shm.shmid, null, 0)); + ss.image.*.data = @ptrCast(ss.shm.shmaddr); + ss.shm.readOnly = c.False; + + _ = c.XShmAttach(display, &ss.shm); + _ = c.XShmGetImage(display, window, ss.image, 0, 0, c.AllPlanes); + + return ss; + } else { + const image = c.XGetImage(display, window, 0, 0, @intCast(attrs.width), @intCast(attrs.height), c.AllPlanes, c.ZPixmap); + if (image == null) return error.ScreenshotFailed; + return .{ .image = image.? }; + } + } + + pub fn deinit(self: *Screenshot) void { + if (build_options.use_xshm) { + _ = XDestroyImage(self.image); + _ = c.shmdt(self.shm.shmaddr); + _ = c.shmctl(self.shm.shmid, c.IPC_RMID, null); + } else { + _ = XDestroyImage(self.image); + } + } +};
\ No newline at end of file diff --git a/src/x11.zig b/src/x11.zig new file mode 100644 index 0000000..0a5d9f3 --- /dev/null +++ b/src/x11.zig @@ -0,0 +1,145 @@ +const std = @import("std"); +const c = @import("c.zig").c; + +pub const X11 = struct { + display: ?*c.Display = null, + root: c.Window = 0, + window: c.Window = 0, + gl_context: c.GLXContext = null, + original_focus_window: c.Window = 0, + screen_width: i32 = 0, + screen_height: i32 = 0, + refresh_rate: i32 = 60, + windowed: bool = false, + wm_delete_window: c.Atom = 0, + + pub fn init() !X11 { + const display = c.XOpenDisplay(null); + if (display == null) { + std.debug.print("error: failed to open display\n", .{}); + return error.X11OpenDisplayFailed; + } + + _ = c.XSetErrorHandler(x11ErrorHandler); + + const root = c.XDefaultRootWindow(display); + var root_attrs: c.XWindowAttributes = undefined; + _ = c.XGetWindowAttributes(display, root, &root_attrs); + + const sc = c.XRRGetScreenInfo(display, root); + const rr = c.XRRConfigCurrentRate(sc); + c.XRRFreeScreenConfigInfo(sc); + + std.debug.print("screen: {d}x{d} @ {d}hz\n", .{ root_attrs.width, root_attrs.height, rr }); + + return .{ + .display = display, + .root = root, + .screen_width = root_attrs.width, + .screen_height = root_attrs.height, + .refresh_rate = if (rr > 0) rr else 60, + }; + } + + pub fn checkGlx(self: *X11) !void { + var glx_major: i32 = undefined; + var glx_minor: i32 = undefined; + if (c.glXQueryVersion(self.display, &glx_major, &glx_minor) == 0 or + (glx_major == 1 and glx_minor < 3) or (glx_major < 1)) + { + std.debug.print("error: invalid glx version\n", .{}); + return error.InvalidGlxVersion; + } + std.debug.print("glx version: {d}.{d}\n", .{ glx_major, glx_minor }); + } + + pub fn createWindow(self: *X11, windowed: bool) !void { + self.windowed = windowed; + + const attrs = [_]c_int{ c.GLX_RGBA, c.GLX_DEPTH_SIZE, 24, c.GLX_DOUBLEBUFFER, 0 }; + const vi = c.glXChooseVisual(self.display, 0, @constCast(&attrs)); + if (vi == null) { + std.debug.print("error: no appropriate visual found\n", .{}); + return error.NoAppropriateVisual; + } + defer _ = c.XFree(vi); + + std.debug.print("visual id: 0x{x}\n", .{vi.*.visualid}); + + var swa: c.XSetWindowAttributes = undefined; + swa.colormap = c.XCreateColormap(self.display, self.root, vi.*.visual, c.AllocNone); + swa.event_mask = c.ButtonPressMask | c.ButtonReleaseMask | c.KeyPressMask | c.KeyReleaseMask | + c.PointerMotionMask | c.ExposureMask | c.ClientMessage; + var mask: c_ulong = c.CWColormap | c.CWEventMask; + + if (!windowed) { + swa.override_redirect = 1; + swa.save_under = 1; + mask |= c.CWOverrideRedirect | c.CWSaveUnder; + } + + self.window = c.XCreateWindow( + self.display, + self.root, + 0, 0, @intCast(self.screen_width), @intCast(self.screen_height), 0, + vi.*.depth, + c.InputOutput, + vi.*.visual, + mask, &swa, + ); + + _ = c.XStoreName(self.display, self.window, "boomer"); + var class_hint = c.XClassHint{ + .res_name = @constCast("boomer"), + .res_class = @constCast("Boomer"), + }; + _ = c.XSetClassHint(self.display, self.window, &class_hint); + + if (windowed) { + self.wm_delete_window = c.XInternAtom(self.display, "WM_DELETE_WINDOW", c.False); + _ = c.XSetWMProtocols(self.display, self.window, &self.wm_delete_window, 1); + } + + _ = c.XMapWindow(self.display, self.window); + + self.gl_context = c.glXCreateContext(self.display, vi, null, c.GL_TRUE); + _ = c.glXMakeCurrent(self.display, self.window, self.gl_context); + + var revert: i32 = 0; + _ = c.XGetInputFocus(self.display, &self.original_focus_window, &revert); + } + + pub fn grabFocus(self: *X11) void { + if (!self.windowed) + _ = c.XSetInputFocus(self.display, self.window, c.RevertToParent, c.CurrentTime); + } + + pub fn restoreFocus(self: *X11) void { + _ = c.XSetInputFocus(self.display, self.original_focus_window, c.RevertToParent, c.CurrentTime); + _ = c.XSync(self.display, c.False); + } + + pub fn getWindowSize(self: *X11, w: *i32, h: *i32) void { + var wa: c.XWindowAttributes = undefined; + _ = c.XGetWindowAttributes(self.display, self.window, &wa); + w.* = wa.width; + h.* = wa.height; + } + + pub fn deinit(self: *X11) void { + if (self.gl_context != null) { + _ = c.glXMakeCurrent(self.display, 0, null); + c.glXDestroyContext(self.display, self.gl_context); + } + if (self.window != 0) _ = c.XDestroyWindow(self.display, self.window); + if (self.display != null) _ = c.XCloseDisplay(self.display); + } +}; + +export fn x11ErrorHandler(d: ?*c.Display, ev: ?*c.XErrorEvent) callconv(.c) c_int { + var buf: [256]u8 = undefined; + _ = c.XGetErrorText(d, ev.?.error_code, &buf, @intCast(buf.len)); + const len = std.mem.indexOfScalar(u8, &buf, 0) orelse buf.len; + std.debug.print("error: x11: {s}\n", .{buf[0..len]}); + return 0; +}
\ No newline at end of file diff --git a/static/demka.gif b/static/demka.gif Binary files differdeleted file mode 100644 index 67de437..0000000 --- a/static/demka.gif +++ /dev/null |
