λ

swayaway

a drop-in replacement for swayidle(1)

settings

main.c

8792 bytes raw

#include "swayaway.h"

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/signalfd.h>
#include <unistd.h>
#include <wordexp.h>
#include <wayland-client.h>

struct swayaway state;

static void usage(const char *prog)
{
	printf("Usage: %s [options] [events...]\n", prog);
	printf(" -C <path>\tpath to config file\n");
	printf(" -h\t\tthis help menu\n");
	printf(" -d\t\tdebug\n");
	printf(" -w\t\twait for command to finish\n");
	printf(" -S\t\tpick the seat to work with\n");
}

static int parse_timeout(int argc, char **argv)
{
	if (argc < 3) {
		sway_log("too few parameters to timeout: "
		         "timeout <seconds> <command> [resume <command>]");
		exit(-1);
	}
	errno = 0;
	char *endptr = NULL;
	long seconds = strtol(argv[1], &endptr, 10);
	if (errno != 0 || endptr == argv[1] || *endptr != '\0') {
		sway_log("invalid timeout '%s': must be a number of seconds", argv[1]);
		exit(-1);
	}

	char *idle_cmd = NULL;
	char *resume_cmd = NULL;
	int consumed = 3;

	if (argc >= 4 && strcmp(argv[2], "resume") == 0) {
		/* legacy mode syntax: `timeout N resume CMD` runs on timeout AND on
		 * resume, which was the behavior before swayidle 1.8 */
		idle_cmd = strdup(argv[3]);
		resume_cmd = strdup(argv[3]);
		consumed = 4;
	} else {
		idle_cmd = strdup(argv[2]);
		if (argc >= 5 && strcmp(argv[3], "resume") == 0) {
			resume_cmd = strdup(argv[4]);
			consumed = 5;
		}
	}

	idle_add_timeout(seconds > 0 ? (int)seconds : -1, idle_cmd, resume_cmd);
	return consumed;
}

static int parse_resume(int argc, char **argv)
{
	// legacy top-level event: `resume CMD` runs on every activity
	if (argc < 2) {
		sway_log("too few parameters to resume: resume <command>");
		exit(-1);
	}
	idle_add_resume_cmd(strdup(argv[1]));
	return 2;
}

static int parse_before_sleep(int argc, char **argv)
{
	if (argc < 2) {
		sway_log("too few parameters to before-sleep: before-sleep <command>");
		exit(-1);
	}
	state.before_sleep_cmd = strdup(argv[1]);
	sway_log_debug("before-sleep: %s", state.before_sleep_cmd);
	return 2;
}

static int parse_after_resume(int argc, char **argv)
{
	if (argc < 2) {
		sway_log("too few parameters to after-resume: after-resume <command>");
		exit(-1);
	}
	state.after_resume_cmd = strdup(argv[1]);
	sway_log_debug("after-resume: %s", state.after_resume_cmd);
	return 2;
}

static int dispatch_event(int argc, char **argv)
{
	const char *name = argv[0];
	if (strcmp(name, "timeout") == 0) {
		return parse_timeout(argc, argv);
	}
	if (strcmp(name, "resume") == 0) {
		return parse_resume(argc, argv);
	}
	if (strcmp(name, "before-sleep") == 0) {
		return parse_before_sleep(argc, argv);
	}
	if (strcmp(name, "after-resume") == 0) {
		return parse_after_resume(argc, argv);
	}
	if (strcmp(name, "lock") == 0 || strcmp(name, "unlock") == 0 ||
	        strcmp(name, "idlehint") == 0) {
		/* swayidle implements these through the logind Session interface;
		 * swayaway intentionally has no logind integration */
		sway_log("'%s' is not supported: swayaway has no logind session "
		         "integration", name);
		exit(-1);
	}
	sway_log("unsupported event '%s'", name);
	return -1;
}

static int parse_args(int argc, char *argv[], char **config_path)
{
	int c;
	while ((c = getopt(argc, argv, "C:hdwS:")) != -1) {
		switch (c) {
			case 'C':
				free(*config_path);
				*config_path = strdup(optarg);
				break;
			case 'd':
				state.debug = true;
				break;
			case 'w':
				state.wait = true;
				break;
			case 'S':
				free(state.seat_name);
				state.seat_name = strdup(optarg);
				break;
			case 'h':
			case '?':
				usage(argv[0]);
				return 1;
		}
	}

	for (int i = optind; i < argc;) {
		int consumed = dispatch_event(argc - i, &argv[i]);
		if (consumed <= 0) {
			return 1;
		}
		i += consumed;
	}
	return 0;
}

static char *get_config_path(void)
{
	/* try swayaway's own paths first, then swayidle's, so an existing
	 * swayidle config works as a drop-in */
	const char *paths[] = {
		"$XDG_CONFIG_HOME/swayaway/config",
		"$HOME/.config/swayaway/config",
		"$HOME/.swayaway/config",
		"$XDG_CONFIG_HOME/swayidle/config",
		"$HOME/.config/swayidle/config",
		"$HOME/.swayidle/config",
	};
	for (size_t i = 0; i < sizeof(paths) / sizeof(paths[0]); i++) {
		wordexp_t p;
		if (wordexp(paths[i], &p, 0) != 0) {
			continue;
		}
		char *path = strdup(p.we_wordv[0]);
		wordfree(&p);
		if (path && access(path, R_OK) == 0) {
			return path;
		}
		free(path);
	}
	return NULL;
}

static int load_config(const char *path)
{
	FILE *f = fopen(path, "r");
	if (!f) {
		return -ENOENT;
	}

	char *line = NULL;
	size_t n = 0;
	ssize_t nread;
	size_t lineno = 0;
	while ((nread = getline(&line, &n, f)) != -1) {
		lineno++;
		if (nread > 0 && line[nread - 1] == '\n') {
			line[nread - 1] = '\0';
		}
		if (strlen(line) == 0 || line[0] == '#') {
			continue;
		}

		wordexp_t p;
		if (wordexp(line, &p, 0) != 0) {
			sway_log("shell expansion error on line %zu", lineno);
			free(line);
			fclose(f);
			return -EINVAL;
		}
		if (p.we_wordc < 1) {
			wordfree(&p);
			continue;
		}
		if (dispatch_event((int)p.we_wordc, p.we_wordv) <= 0) {
			sway_log("error on line %zu of %s", lineno, path);
			wordfree(&p);
			free(line);
			fclose(f);
			return -EINVAL;
		}
		wordfree(&p);
	}
	free(line);
	fclose(f);
	return 0;
}

int main(int argc, char *argv[])
{
	state.sleep_lock_fd = -1;
	state.running = true;

	char *config_path = NULL;
	if (parse_args(argc, argv, &config_path) != 0) {
		free(config_path);
		return 1;
	}
	log_init(state.debug);

	if (!config_path) {
		config_path = get_config_path();
	}
	if (config_path) {
		int r = load_config(config_path);
		if (r == -EINVAL) {
			sway_log("config file %s has errors, exiting", config_path);
			free(config_path);
			return 1;
		} else if (r == 0) {
			sway_log("loaded config from %s", config_path);
		}
	}
	free(config_path);

	// block the signals handled via signalfd; these turn into poll events
	sigset_t mask;
	sigemptyset(&mask);
	sigaddset(&mask, SIGINT);
	sigaddset(&mask, SIGTERM);
	sigaddset(&mask, SIGUSR1);
	sigprocmask(SIG_BLOCK, &mask, NULL);
	int sigfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
	if (sigfd < 0) {
		sway_log("signalfd failed: %s", strerror(errno));
		return 1;
	}

	if (wayland_init() != 0) {
		return 1;
	}

	screensaver_init();

	if (state.before_sleep_cmd || state.after_resume_cmd) {
		logind_init();
	}

	if (!idle_has_work()) {
		sway_log("no events configured; nothing to do, exiting");
		return 0;
	}

	wayland_create_tracking_notification();

	// the wayland fd, the two optional bus fds, and the signalfd
	struct pollfd fds[4];
	int wl_idx = 0;
	int session_idx = -1;
	int system_idx = -1;
	int sig_idx;

	while (state.running) {
		int timeout = idle_next_timeout_ms();
		int nfds = 0;

		fds[nfds].fd = wl_display_get_fd(state.display);
		fds[nfds].events = POLLIN;
		wl_idx = nfds++;

		if (state.session_conn) {
			fds[nfds].fd = state.session_fd;
			fds[nfds].events = POLLIN;
			session_idx = nfds++;
		} else {
			session_idx = -1;
		}
		if (state.system_conn) {
			fds[nfds].fd = state.system_fd;
			fds[nfds].events = POLLIN;
			system_idx = nfds++;
		} else {
			system_idx = -1;
		}

		fds[nfds].fd = sigfd;
		fds[nfds].events = POLLIN;
		sig_idx = nfds++;

		int r = poll(fds, nfds, timeout);
		if (r < 0) {
			if (errno == EINTR) {
				continue;
			}
			sway_log("poll failed: %s", strerror(errno));
			break;
		}

		if (fds[wl_idx].revents) {
			if (fds[wl_idx].revents & (POLLERR | POLLHUP)) {
				sway_log("wayland connection closed");
				break;
			}
			if (fds[wl_idx].revents & POLLIN) {
				/* non-blocking read: avoid wl_display_dispatch's internal
				 * poll on spurious wakeups */
				if (wl_display_prepare_read(state.display) == -1) {
					wl_display_dispatch_pending(state.display);
				} else {
					wl_display_read_events(state.display);
					wl_display_dispatch_pending(state.display);
				}
			}
		}

		if (session_idx >= 0 && fds[session_idx].revents) {
			if (fds[session_idx].revents & (POLLERR | POLLHUP)) {
				sway_log("session bus closed");
				break;
			}
			dbus_connection_read_write_dispatch(state.session_conn, 0);
		}

		if (system_idx >= 0 && fds[system_idx].revents) {
			if (fds[system_idx].revents & (POLLERR | POLLHUP)) {
				sway_log("system bus closed");
				break;
			}
			dbus_connection_read_write_dispatch(state.system_conn, 0);
		}

		if (fds[sig_idx].revents & POLLIN) {
			struct signalfd_siginfo si;
			while (read(sigfd, &si, sizeof(si)) == sizeof(si)) {
				if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) {
					idle_terminate();
				} else if (si.ssi_signo == SIGUSR1) {
					idle_force_idle();
				}
			}
		}

		idle_check_timeouts();
	}

	logind_release_sleep_lock();
	return 0;
}