aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorkj_sh6042026-06-05 16:08:47 -0400
committerkj_sh6042026-06-05 16:08:47 -0400
commitffb0182d90d5607ccccff5210a2e711d6af35458 (patch)
tree3bb0cee0f2917a66d735b1e08797da0dd9666f45
parent8c3af04bf55c334500252faca56fae61429fb770 (diff)
refactor: zig re-implementation
-rw-r--r--.gitignore5
-rw-r--r--Makefile30
-rw-r--r--README13
-rw-r--r--build.zig30
-rw-r--r--src/app.zig208
-rw-r--r--src/c.zig15
-rw-r--r--src/config.h89
-rw-r--r--src/config.zig133
-rw-r--r--src/kj-boomer.c809
-rw-r--r--src/la.h45
-rw-r--r--src/main.zig178
-rw-r--r--src/math.zig32
-rw-r--r--src/opengl.zig283
-rw-r--r--src/screenshot.h96
-rw-r--r--src/screenshot.zig59
-rw-r--r--src/x11.zig145
-rw-r--r--static/demka.gifbin1059927 -> 0 bytes
17 files changed, 1105 insertions, 1065 deletions
diff --git a/.gitignore b/.gitignore
index 15b1ff0..764eb9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,7 @@
boomer
release
TAGS
-digest.txt \ No newline at end of file
+digest.txt
+zig-out/
+zig-cache/
+.zig-cache/
diff --git a/Makefile b/Makefile
index 5d63e8f..5a40f94 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README b/README
index f8bf57a..8698c30 100644
--- a/README
+++ b/README
@@ -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
deleted file mode 100644
index 67de437..0000000
--- a/static/demka.gif
+++ /dev/null
Binary files differ