view parser/parser.c @ 1:50bab568625d default tip

lol
author tsukasa <qwerasdnoob@gmail.com>
date Sat, 11 Jul 2026 17:56:41 +0800
parents
children
line wrap: on
line source

/* Here be a small dragon.
 * Not as large as the monster 
 * in src/swindle.c
 * but you get the idea */

#include "parser.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/inotify.h>
#include <linux/input-event-codes.h>
#include <xkbcommon/xkbcommon.h>
#include <wayland-server-core.h>

/* Lua */
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

/* wlroots */
#include <wlr/util/log.h>
#include <wlr/types/wlr_keyboard.h>
#include <libinput.h>

/* Forward declarations from src/swindle.c  */
extern struct wl_list mons;   
extern struct wl_list clients; 
void arrange(void *m);       

/* Internal watcher state */

typedef struct {
	int                  inotify_fd;
	int                  watch_fd;
	char                 path[1024];
	config_reload_cb     cb;
	void                *userdata;
	struct wl_event_source *event_src;
} WatchState;

/* Helpers */

static void
lua_log_error(lua_State *L, const char *context)
{
	fprintf(stderr, "swindle config [%s]: %s\n",
	        context, lua_tostring(L, -1));
	lua_pop(L, 1);
}

/* Get a string field from the table at the top of the stack
 * Writes into buf and returns false and leaves buf untouched on failure
 */

static bool
lua_get_string(lua_State *L, const char *key, char *buf, size_t bufsz)
{
	lua_getfield(L, -1, key);
	bool ok = lua_isstring(L, -1);
	if (ok)
		strncpy(buf, lua_tostring(L, -1), bufsz - 1);
	lua_pop(L, 1);
	return ok;
}

/* Get an integer field. It falls back to `def` if missing or wrong type */

static int
lua_get_int(lua_State *L, const char *key, int def)
{
	lua_getfield(L, -1, key);
	int val = lua_isnumber(L, -1) ? (int)lua_tointeger(L, -1) : def;
	lua_pop(L, 1);
	return val;
}

static double
lua_get_double(lua_State *L, const char *key, double def)
{
	lua_getfield(L, -1, key);
	double val = lua_isnumber(L, -1) ? lua_tonumber(L, -1) : def;
	lua_pop(L, 1);
	return val;
}

static bool
lua_get_bool(lua_State *L, const char *key, bool def)
{
	lua_getfield(L, -1, key);
	bool val = lua_isboolean(L, -1) ? lua_toboolean(L, -1) : def;
	lua_pop(L, 1);
	return val;
}

static void
parse_color_hex(uint32_t hex, float out[4])
{
	out[0] = ((hex >> 24) & 0xFF) / 255.0f;
	out[1] = ((hex >> 16) & 0xFF) / 255.0f;
	out[2] = ((hex >>  8) & 0xFF) / 255.0f;
	out[3] = ( hex        & 0xFF) / 255.0f;
}

static void
lua_get_color(lua_State *L, const char *key, float out[4],
              float r, float g, float b, float a)
{
	lua_getfield(L, -1, key);
	if (lua_isnumber(L, -1))
		parse_color_hex((uint32_t)lua_tointeger(L, -1), out);
	else {
		out[0] = r; out[1] = g; out[2] = b; out[3] = a;
	}
	lua_pop(L, 1);
}

static uint32_t
parse_modifier(const char *name)
{
	if (!strcmp(name, "shift")) return WLR_MODIFIER_SHIFT;
	if (!strcmp(name, "ctrl"))  return WLR_MODIFIER_CTRL;
	if (!strcmp(name, "alt"))   return WLR_MODIFIER_ALT;
	if (!strcmp(name, "logo"))  return WLR_MODIFIER_LOGO;
	if (!strcmp(name, "mod2"))  return WLR_MODIFIER_MOD2;
	if (!strcmp(name, "mod3"))  return WLR_MODIFIER_MOD3;
	if (!strcmp(name, "mod5"))  return WLR_MODIFIER_MOD5;
	fprintf(stderr, "swindle config: unknown modifier '%s', ignoring\n", name);
	return 0;
}

/* Section parsers */

static void
parse_appearance(lua_State *L, CfgAppearance *a)
{
	lua_getglobal(L, "appearance");
	if (!lua_istable(L, -1)) {
		fprintf(stderr, "swindle config: 'appearance' table missing' \n");
		lua_pop(L, 1);
		/* defaults */
		a->inner_border_px = 1;
		a->outer_border_px = 1;
		a->gaps       = 10;
		a->smart_gaps = false;
		parse_color_hex(0x222222ff, a->root_color);
		parse_color_hex(0x00000000, a->inner_border_color);
		parse_color_hex(0x00000000, a->outer_border_color);
		parse_color_hex(0xe8e8e8ff, a->inner_focus_color);
		parse_color_hex(0xe8e8e8ff, a->outer_focus_color);
		parse_color_hex(0xff0000ff, a->inner_urgent_color);
		parse_color_hex(0xff0000ff, a->outer_urgent_color);
		a->fullscreen_bg[3] = 1.0f;
		return;
	}

	a->inner_border_px = (unsigned int)lua_get_int(L, "inner_border_px", 1);
	a->outer_border_px = (unsigned int)lua_get_int(L, "outer_border_px", 1);
	a->gaps       = (unsigned int)lua_get_int(L, "gaps",       10);
	a->smart_gaps = lua_get_bool(L, "smart_gaps", false);

	lua_get_color(L, "root_color",          a->root_color,          0.133f,0.133f,0.133f,1.f);
	lua_get_color(L, "inner_border_color",  a->inner_border_color,  0.f,0.f,0.f,0.f);
	lua_get_color(L, "outer_border_color",  a->outer_border_color,  0.f,0.f,0.f,0.f);
	lua_get_color(L, "inner_focus_color",   a->inner_focus_color,   0.91f,0.91f,0.91f,1.f);
	lua_get_color(L, "outer_focus_color",   a->outer_focus_color,   0.91f,0.91f,0.91f,1.f);
	lua_get_color(L, "inner_urgent_color",  a->inner_urgent_color,  1.f,0.f,0.f,1.f);
	lua_get_color(L, "outer_urgent_color",  a->outer_urgent_color,  1.f,0.f,0.f,1.f);

	lua_getfield(L, -1, "fullscreen_bg");
	if (lua_isnumber(L, -1))
		parse_color_hex((uint32_t)lua_tointeger(L, -1), a->fullscreen_bg);
	else {
		a->fullscreen_bg[0] = 0.f;
		a->fullscreen_bg[1] = 0.f;
		a->fullscreen_bg[2] = 0.f;
		a->fullscreen_bg[3] = 1.f;
	}
	lua_pop(L, 1);

	lua_pop(L, 1); /* pop appearance table */
}

static void
parse_input(lua_State *L, CfgInput *inp)
{
	lua_getglobal(L, "input");
	if (!lua_istable(L, -1)) {
		fprintf(stderr, "swindle config: 'input' table missing, "
		        "using defaults. You'll feel the repeat delay.\n");
		lua_pop(L, 1);
		inp->repeat_rate  = 50;
		inp->repeat_delay = 150;
		inp->tap_to_click = true;
		inp->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE;
		return;
	}

	inp->repeat_rate            = lua_get_int (L, "repeat_rate",            50);
	inp->repeat_delay           = lua_get_int (L, "repeat_delay",           150);
	inp->tap_to_click           = lua_get_bool(L, "tap_to_click",           true);
	inp->tap_and_drag           = lua_get_bool(L, "tap_and_drag",           true);
	inp->drag_lock              = lua_get_bool(L, "drag_lock",              true);
	inp->natural_scrolling      = lua_get_bool(L, "natural_scrolling",      false);
	inp->disable_while_typing   = lua_get_bool(L, "disable_while_typing",   true);
	inp->left_handed            = lua_get_bool(L, "left_handed",            false);
	inp->middle_button_emulation= lua_get_bool(L, "middle_button_emulation",false);
	inp->accel_speed            = lua_get_double(L, "accel_speed",          0.0);

	char scratch[CFG_MAX_STRLEN] = {0};

	lua_get_string(L, "scroll_method", scratch, sizeof(scratch));
	if      (!strcmp(scratch, "2fg"))    inp->scroll_method = LIBINPUT_CONFIG_SCROLL_2FG;
	else if (!strcmp(scratch, "edge"))   inp->scroll_method = LIBINPUT_CONFIG_SCROLL_EDGE;
	else if (!strcmp(scratch, "button")) inp->scroll_method = LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN;
	else                                 inp->scroll_method = LIBINPUT_CONFIG_SCROLL_2FG;

	lua_get_string(L, "accel_profile", scratch, sizeof(scratch));
	if (!strcmp(scratch, "flat"))        inp->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_FLAT;
	else                                 inp->accel_profile = LIBINPUT_CONFIG_ACCEL_PROFILE_ADAPTIVE;

	lua_get_string(L, "click_method", scratch, sizeof(scratch));
	if      (!strcmp(scratch, "clickfinger"))   inp->click_method = LIBINPUT_CONFIG_CLICK_METHOD_CLICKFINGER;
	else if (!strcmp(scratch, "button_areas"))  inp->click_method = LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS;
	else                                        inp->click_method = LIBINPUT_CONFIG_CLICK_METHOD_BUTTON_AREAS;

	lua_get_string(L, "send_events_mode", scratch, sizeof(scratch));
	if      (!strcmp(scratch, "disabled"))             inp->send_events_mode = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED;
	else if (!strcmp(scratch, "disabled_on_external")) inp->send_events_mode = LIBINPUT_CONFIG_SEND_EVENTS_DISABLED_ON_EXTERNAL_MOUSE;
	else                                               inp->send_events_mode = LIBINPUT_CONFIG_SEND_EVENTS_ENABLED;

	lua_get_string(L, "tap_button_map", scratch, sizeof(scratch));
	if (!strcmp(scratch, "lmr")) inp->tap_button_map = LIBINPUT_CONFIG_TAP_MAP_LMR;
	else                         inp->tap_button_map = LIBINPUT_CONFIG_TAP_MAP_LRM;

	lua_pop(L, 1);
}

static void
parse_rules(lua_State *L, Config *cfg)
{
	lua_getglobal(L, "rules");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		return;
	}

	cfg->nrules = 0;
	int n = (int)lua_rawlen(L, -1);
	for (int i = 1; i <= n && cfg->nrules < CFG_MAX_RULES; i++) {
		lua_rawgeti(L, -1, i);
		if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }

		CfgRule *r = &cfg->rules[cfg->nrules++];
		memset(r, 0, sizeof(*r));
		lua_get_string(L, "app_id", r->app_id, sizeof(r->app_id));
		lua_get_string(L, "title",  r->title,  sizeof(r->title));
		r->tags     = (uint32_t)lua_get_int(L, "tags",     0);
		r->floating = lua_get_bool(L, "floating", false);
		r->monitor  = lua_get_int(L, "monitor", -1);

		lua_pop(L, 1);
	}
	lua_pop(L, 1);
}

static void
parse_monitors(lua_State *L, Config *cfg)
{
	lua_getglobal(L, "monitors");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		cfg->nmonitors = 1;
		CfgMonitorRule *m = &cfg->monitors[0];
		memset(m, 0, sizeof(*m));
		m->mfact   = 0.55f;
		m->nmaster = 1;
		m->scale   = 1.0f;
		strcpy(m->layout, "dwindle");
		m->x = m->y = -1;
		return;
	}

	cfg->nmonitors = 0;
	int n = (int)lua_rawlen(L, -1);
	for (int i = 1; i <= n && cfg->nmonitors < CFG_MAX_MONITORS; i++) {
		lua_rawgeti(L, -1, i);
		if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }

		CfgMonitorRule *m = &cfg->monitors[cfg->nmonitors++];
		memset(m, 0, sizeof(*m));

		lua_getfield(L, -1, "name");
		if (lua_isstring(L, -1))
			strncpy(m->name, lua_tostring(L, -1), sizeof(m->name)-1);
		lua_pop(L, 1);

		m->mfact   = (float)lua_get_double(L, "mfact",   0.55);
		m->nmaster =        lua_get_int   (L, "nmaster", 1);
		m->scale   = (float)lua_get_double(L, "scale",   1.0);
		m->x       =        lua_get_int   (L, "x",       -1);
		m->y       =        lua_get_int   (L, "y",       -1);
		m->transform =      lua_get_int   (L, "transform", 0);
		lua_get_string(L, "layout", m->layout, sizeof(m->layout));
		if (!m->layout[0]) strcpy(m->layout, "dwindle");

		lua_pop(L, 1);
	}
	lua_pop(L, 1);
}

static void
parse_keybinds(lua_State *L, Config *cfg)
{
	lua_getglobal(L, "keybinds");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		return;
	}

	cfg->nkeybinds = 0;
	struct xkb_context *xkb_ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);

	int n = (int)lua_rawlen(L, -1);
	for (int i = 1; i <= n && cfg->nkeybinds < CFG_MAX_KEYBINDS; i++) {
		lua_rawgeti(L, -1, i);
		if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }

		CfgKeybind *kb = &cfg->keybinds[cfg->nkeybinds];
		memset(kb, 0, sizeof(*kb));

		/* mods: array of strings */
		kb->mods = 0;
		lua_getfield(L, -1, "mods");
		if (lua_istable(L, -1)) {
			int nm = (int)lua_rawlen(L, -1);
			for (int m = 1; m <= nm; m++) {
				lua_rawgeti(L, -1, m);
				if (lua_isstring(L, -1))
					kb->mods |= parse_modifier(lua_tostring(L, -1));
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1);

		/* key: string keysym name, e.g. "q", "Return", "XF86AudioMute" */
		char keyname[CFG_MAX_STRLEN] = {0};
		lua_get_string(L, "key", keyname, sizeof(keyname));
		kb->key = xkb_keysym_from_name(keyname, XKB_KEYSYM_CASE_INSENSITIVE);
		if (kb->key == XKB_KEY_NoSymbol) {
			fprintf(stderr, "swindle config: unknown key '%s', skipping bind\n",
			        keyname);
			lua_pop(L, 1); /* keybind entry table */
			continue;
		}

		lua_get_string(L, "action", kb->action, sizeof(kb->action));

		/* args: optional array of strings */
		kb->nargs = 0;
		lua_getfield(L, -1, "args");
		if (lua_istable(L, -1)) {
			int na = (int)lua_rawlen(L, -1);
			for (int a = 1; a <= na && kb->nargs < CFG_MAX_ARGS; a++) {
				lua_rawgeti(L, -1, a);
				if (lua_isstring(L, -1))
					strncpy(kb->args[kb->nargs++],
					        lua_tostring(L, -1),
					        CFG_MAX_STRLEN - 1);
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1); /* args */

		cfg->nkeybinds++;
		lua_pop(L, 1); /* keybind entry */
	}

	xkb_context_unref(xkb_ctx);
	lua_pop(L, 1); /* keybinds table */
}

static void
parse_buttons(lua_State *L, Config *cfg)
{
	lua_getglobal(L, "buttons");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		return;
	}

	cfg->nbuttons = 0;
	int n = (int)lua_rawlen(L, -1);
	for (int i = 1; i <= n && cfg->nbuttons < CFG_MAX_KEYBINDS; i++) {
		lua_rawgeti(L, -1, i);
		if (!lua_istable(L, -1)) { lua_pop(L, 1); continue; }

		CfgButton *b = &cfg->buttons[cfg->nbuttons++];
		memset(b, 0, sizeof(*b));

		b->mods = 0;
		lua_getfield(L, -1, "mods");
		if (lua_istable(L, -1)) {
			int nm = (int)lua_rawlen(L, -1);
			for (int m = 1; m <= nm; m++) {
				lua_rawgeti(L, -1, m);
				if (lua_isstring(L, -1))
					b->mods |= parse_modifier(lua_tostring(L, -1));
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1);

		char btnname[CFG_MAX_STRLEN] = {0};
		lua_get_string(L, "button", btnname, sizeof(btnname));
		if      (!strcmp(btnname, "left"))   b->button = BTN_LEFT;
		else if (!strcmp(btnname, "right"))  b->button = BTN_RIGHT;
		else if (!strcmp(btnname, "middle")) b->button = BTN_MIDDLE;
		else b->button = (uint32_t)atoi(btnname); 

		lua_get_string(L, "action", b->action, sizeof(b->action));

		b->nargs = 0;
		lua_getfield(L, -1, "args");
		if (lua_istable(L, -1)) {
			int na = (int)lua_rawlen(L, -1);
			for (int a = 1; a <= na && b->nargs < CFG_MAX_ARGS; a++) {
				lua_rawgeti(L, -1, a);
				if (lua_isstring(L, -1))
					strncpy(b->args[b->nargs++],
					        lua_tostring(L, -1),
					        CFG_MAX_STRLEN - 1);
				lua_pop(L, 1);
			}
		}
		lua_pop(L, 1); /* args */

		lua_pop(L, 1);
	}
	lua_pop(L, 1);
}

static void
parse_autostart(lua_State *L, Config *cfg)
{
	lua_getglobal(L, "autostart");
	if (!lua_istable(L, -1)) {
		lua_pop(L, 1);
		return;
	}

	cfg->nautostart = 0;
	int n = (int)lua_rawlen(L, -1);
	for (int i = 1; i <= n && cfg->nautostart < CFG_MAX_AUTOSTART; i++) {
		lua_rawgeti(L, -1, i);
		if (!lua_isstring(L, -1)) {
			lua_pop(L, 1);
			continue;
		}
		strncpy(cfg->autostart[cfg->nautostart].cmd,
		        lua_tostring(L, -1),
		        CFG_MAX_AUTOSTART_CMD - 1);
		cfg->nautostart++;
		lua_pop(L, 1);
	}
	lua_pop(L, 1);
}

/* Public API */

char *
config_get_path(char *buf, size_t bufsz)
{
	const char *xdg = getenv("XDG_CONFIG_HOME");
	if (xdg && *xdg)
		snprintf(buf, bufsz, "%s/swindle/config.lua", xdg);
	else {
		const char *home = getenv("HOME");
		if (!home) home = "/root"; /* you're probably root, aren't you */
		snprintf(buf, bufsz, "%s/.config/swindle/config.lua", home);
	}
	return buf;
}

int
config_load(const char *path, Config *cfg)
{
	memset(cfg, 0, sizeof(*cfg));

	lua_State *L = luaL_newstate();
	if (!L) {
		fprintf(stderr, "swindle config: failed to create Lua state. "
		        "Truly rock bottom.\n");
		return -1;
	}
	luaL_openlibs(L);

	if (luaL_dofile(L, path) != LUA_OK) {
		lua_log_error(L, "load");
		lua_close(L);
		return -1;
	}

	/* Top-level booleans */
	lua_getglobal(L, "sloppy_focus");
	cfg->sloppyfocus = lua_isboolean(L, -1) ? lua_toboolean(L, -1) : true;
	lua_pop(L, 1);

	lua_getglobal(L, "bypass_surface_visibility");
	cfg->bypass_surface_visibility = lua_isboolean(L, -1)
	                                 ? lua_toboolean(L, -1) : false;
	lua_pop(L, 1);

	lua_getglobal(L, "log_level");
	if (lua_isstring(L, -1)) {
		const char *ll = lua_tostring(L, -1);
		if      (!strcmp(ll, "silent")) cfg->log_level = WLR_SILENT;
		else if (!strcmp(ll, "info"))   cfg->log_level = WLR_INFO;
		else if (!strcmp(ll, "debug"))  cfg->log_level = WLR_DEBUG;
		else                            cfg->log_level = WLR_ERROR;
	} else {
		cfg->log_level = WLR_ERROR;
	}
	lua_pop(L, 1);

	parse_appearance(L, &cfg->appearance);
	parse_input     (L, &cfg->input);
	parse_rules     (L, cfg);
	parse_monitors  (L, cfg);
	parse_keybinds  (L, cfg);
	parse_buttons   (L, cfg);
	parse_autostart (L, cfg);

	lua_close(L);
	return 0;
}

/* Apply functions */

void
config_apply_appearance(const Config *cfg)
{
	(void)cfg;
}

void
config_apply_input(const Config *cfg)
{
	(void)cfg;
}

void
config_apply_keybinds(const Config *cfg)
{
	(void)cfg;
}

void
config_autostart_run(const Config *cfg)
{
	if (cfg->nautostart == 0)
		return;

	pid_t pid = fork();
	if (pid < 0) {
		perror("swindle autostart: fork");
		return;
	}
	if (pid != 0)
		return; /* compositor continues (sequencer runs in child) */

	/* The sequencer child resets any inherited signal handlers and becomes
	 * a new session so the compositor's SIGCHLD handling doesn't
	 * interfere with our waitpid calls. */
	signal(SIGCHLD, SIG_DFL);
	setsid();

	for (int i = 0; i < cfg->nautostart; i++) {
		const char *cmd = cfg->autostart[i].cmd;

		pid_t cpid = fork();
		if (cpid < 0) {
			fprintf(stderr, "swindle autostart: fork for '%s': %s\n",
			        cmd, strerror(errno));
			continue;
		}
		if (cpid == 0) {
			execl("/bin/sh", "sh", "-c", cmd, NULL);
			fprintf(stderr, "swindle autostart: exec '%s': %s\n",
			        cmd, strerror(errno));
			_exit(1);
		}

		/* wait a second because one shotters will fly past, but
		 * daemon processes (some, like swaybg) won't because of 
		 * daemon black magic. To say the least, it's good for the soul */
		struct timespec deadline, now;
		clock_gettime(CLOCK_MONOTONIC, &deadline);
		deadline.tv_sec += 0.5;

		int status;
		int done = 0;
		while (!done) {
			pid_t r = waitpid(cpid, &status, WNOHANG);
			if (r == cpid) {
				done = 1;
				if (WIFEXITED(status) && WEXITSTATUS(status) != 0)
					fprintf(stderr, "swindle autostart: '%s' exited with status %d\n",
					        cmd, WEXITSTATUS(status));
			} else {
				clock_gettime(CLOCK_MONOTONIC, &now);
				if (now.tv_sec > deadline.tv_sec ||
				    (now.tv_sec == deadline.tv_sec &&
				     now.tv_nsec >= deadline.tv_nsec))
					break; /* looks like a daemon, move on */
				struct timespec sl = { .tv_sec = 0, .tv_nsec = 50 * 1000 * 1000 };
				nanosleep(&sl, NULL);
			}
		}
	}

	_exit(0);
}

/* inotify watcher */

static int
watch_dispatch(int fd, uint32_t mask, void *data)
{
	WatchState *ws = data;

	/* Drain the inotify events */
	_Alignas(struct inotify_event) char buf[4096];
	ssize_t len = read(fd, buf, sizeof(buf));
	if (len < 0) {
		if (errno != EAGAIN)
			perror("swindle config: inotify read");
		return 0;
	}

	/* Re-parse */ 
	Config *fresh = calloc(1, sizeof(Config));
	if (!fresh) return 0;

	if (config_load(ws->path, fresh) == 0) {
		fprintf(stderr, "swindle config: reloaded '%s'\n", ws->path);
		if (ws->cb)
			ws->cb(fresh, ws->userdata);
	} else {
		fprintf(stderr, "swindle config: reload failed, keeping old config. "
		        "Story of my life.\n");
	}

	free(fresh);

	/*
	 * Some editors (vim, nano) delete-and-recreate files on save,
	 * which removes our watch (which is a pain in the ass. 
	 * Hence, re-add it defensively. It's good for the soul)
	 */
	inotify_rm_watch(ws->inotify_fd, ws->watch_fd);
	ws->watch_fd = inotify_add_watch(ws->inotify_fd, ws->path,
	                                  IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE);

	return 0;
}

struct wl_event_source *
config_watch_start(struct wl_event_loop *loop, const char *path,
                   config_reload_cb cb, void *userdata)
{
	WatchState *ws = calloc(1, sizeof(*ws));
	if (!ws) return NULL;

	ws->inotify_fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
	if (ws->inotify_fd < 0) {
		perror("swindle config: inotify_init1");
		free(ws);
		return NULL;
	}

	ws->watch_fd = inotify_add_watch(ws->inotify_fd, path,
	                                  IN_CLOSE_WRITE | IN_MOVED_TO | IN_CREATE);
	if (ws->watch_fd < 0) {
		perror("swindle config: inotify_add_watch");
		close(ws->inotify_fd);
		free(ws);
		return NULL;
	}

	strncpy(ws->path, path, sizeof(ws->path) - 1);
	ws->cb       = cb;
	ws->userdata = userdata;

	struct wl_event_source *src = wl_event_loop_add_fd(
		loop, ws->inotify_fd,
		WL_EVENT_READABLE,
		watch_dispatch, ws
	);

	if (!src) {
		fprintf(stderr, "swindle config: failed to add inotify to event loop. "
		        "Classic.\n");
		close(ws->inotify_fd);
		free(ws);
		return NULL;
	}

	ws->event_src = src;
	fprintf(stderr, "swindle config: watching '%s'\n", path);
	return (struct wl_event_source *)ws;
}

void
config_watch_stop(struct wl_event_source *src)
{
	if (!src) return;
	WatchState *ws = (WatchState *)src;
	wl_event_source_remove(ws->event_src);
	close(ws->inotify_fd); 
	free(ws);
}