diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..646a8c7b843335322d5dc63265e0b584e4ec9e79 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +MIT License Copyright (c) 2026 nora aoki + +Permission is hereby granted, free of charge, +to any person obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above +copyright notice and this permission notice (including the next paragraph) shall +be included in all copies or substantial portions of the Software. + +THE SOFTWARE +IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING +BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..dc32c6c971bb3ade4441ff3aa4167992027f9a3e --- /dev/null +++ b/Makefile @@ -0,0 +1,61 @@ +CC ?= cc +PKG_CONFIG ?= pkg-config +WAYLAND_SCANNER ?= wayland-scanner + +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +MANDIR ?= $(PREFIX)/share/man +SCODOC ?= scdoc + +WAYLAND_PROTOCOLS_DIR := $(shell $(PKG_CONFIG) --variable=pagedir wayland-protocols 2>/dev/null) +ifeq ($(WAYLAND_PROTOCOLS_DIR),) +WAYLAND_PROTOCOLS_DIR := /usr/share/wayland-protocols +endif +EXT_IDLE_XML := $(firstword \ + $(wildcard $(WAYLAND_PROTOCOLS_DIR)/stable/ext-idle-notify/ext-idle-notify-v1.xml) \ + $(wildcard $(WAYLAND_PROTOCOLS_DIR)/staging/ext-idle-notify/ext-idle-notify-v1.xml)) +ifeq ($(EXT_IDLE_XML),) +$(error ext-idle-notify-v1.xml not found under $(WAYLAND_PROTOCOLS_DIR); install wayland-protocols >= 1.32) +endif + +CFLAGS ?= -O2 -Wall -Wextra -std=c99 +CFLAGS += $(shell $(PKG_CONFIG) --cflags wayland-client dbus-1) +LDLIBS += $(shell $(PKG_CONFIG) --libs wayland-client dbus-1) + +GENERATED = ext-idle-notify-v1-client-protocol.h ext-idle-notify-v1-protocol.c + +MANPAGE = swayaway.1 + +SRCS = main.c log.c idle.c wayland.c screensaver.c logind.c \ + ext-idle-notify-v1-protocol.c +OBJS = $(SRCS:.c=.o) + +all: swayaway + +swayaway: $(OBJS) + $(CC) $(CFLAGS) -o $@ $(OBJS) $(LDLIBS) + +$(MANPAGE): swayaway.1.scd + $(SCODOC) < $< > $@ + +man: $(MANPAGE) + +%.o: %.c swayaway.h $(GENERATED) + $(CC) $(CFLAGS) -c -o $@ $< + +ext-idle-notify-v1-client-protocol.h: $(EXT_IDLE_XML) + $(WAYLAND_SCANNER) client-header $< $@ + +ext-idle-notify-v1-protocol.c: $(EXT_IDLE_XML) + $(WAYLAND_SCANNER) private-code $< $@ + +install: swayaway + install -Dm755 swayaway $(DESTDIR)$(BINDIR)/swayaway + +install-man: $(MANPAGE) + install -Dm644 $(MANPAGE) $(DESTDIR)$(MANDIR)/man1/swayaway.1 + +clean: + rm -f $(OBJS) swayaway $(GENERATED) $(MANPAGE) + +.PHONY: all clean install install-man man diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..33b3d0a393a2da380f123d084e7f7dc3ee2391d3 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# swayaway + +a drop-in replacement for [`swayidle(1)`](https://github.com/swaywm/swayidle) for Wayland compositors, written in C99, with no elogind dependency and no libsystemd. + +the only dependencies are `libwayland-client` and `libdbus-1`. + +this exports `org.freedesktop.ScreenSaver` (plus the `org.gnome.ScreenSaver` alias) so browsers, media players, and any other app that calls `Inhibit()`/`Uninhibit()` can suppress idle timeouts. + +it also listens for logind's `PrepareForSleep` signal to run `before-sleep` / `after-resume` commands. if no logind is running, these events simply never fire. + +## usage + +``` +swayaway [options] [events...] +``` + +options: + +| option | meaning | +|-------------|--------------------------------------------------------------------------| +| `-C ` | config file (default: see below) | +| `-d` | debug output | +| `-w` | wait for commands to finish (useful with `before-sleep` + swaylock `-f`) | +| `-S ` | pick a specific seat | +| `-h` | help | + +events: + +``` +timeout [resume ] +resume # legacy: run on every activity +before-sleep +after-resume +``` + +all commands will be executed via `/bin/sh -c`. this means you can use env vars, pipes, and wildcards in your command. because of this, you should quote the entire command string if it contains spaces, both on the command line and in the config file + +example (the classic sway setup): + +```sh +swayaway -w \ + timeout 300 'swaylock -f -c 000000' \ + timeout 600 'swaymsg "output * power off"' \ + resume 'swaymsg "output * power on"' \ + before-sleep 'swaylock -f -c 000000' +``` + +the `timeout ... resume ...` two-command form is the swayidle 1.8+/1.9 syntax; `timeout N resume CMD` (old pre-1.8 mode syntax) is also accepted + +the top-level `resume CMD` event (used by the famous sway wiki example) works too. + +swayidle's `lock`, `unlock` and `idlehint` events are *not* supported (they go through the logind Session interface), swayaway rejects them with an error, same as swayidle built without systemd/elogind + +### config file + +without `-C`, swayaway checks, in order: + +1. `$XDG_CONFIG_HOME/swayaway/config` +2. `$HOME/.config/swayaway/config` +3. `$XDG_CONFIG_HOME/swayidle/config` +4. `$HOME/.config/swayidle/config` +5. `$HOME/.swayidle/config` + +so an existing swayidle config works unchanged. Config lines are the same +events as the command line; `#` starts a comment and shell expansion applies +(quote commands with spaces: `timeout 300 "swaylock -f"`). + +## how it works + +this basically polls 4 fds, wayland fd, session bus fd, system bus fd, and a signalfd like so: +``` +poll( [ wayland_fd, session_bus_fd, system_bus_fd, signalfd ], timeout ) +``` + +- wayland fd is a single `ext-idle-notify-v1` notification with timeout 0 tracks idle/activity; the compositor sends `idled` as soon as the seat is idle and `resumed` on the first input. +- monotonic clock `CLOCK_MONOTONIC` records when idle started; all timeout deadlines are computed from it, and the next deadline becomes the `poll()` timeout. +- the session bus fd is the ScreenSaver service. `Inhibit` returns a cookie, `Uninhibit` releases it; cookies are released automatically if their client disconnects. while any inhibitor is held, timeouts are frozen (and idle counting restarts when the last inhibitor is released, mirroring swayidle re-registering its timers). +- system bus fd `PrepareForSleep`: run `before-sleep`, then release the logind "delay" sleep lock so suspend actually happens; on resume run `after-resume` and re-acquire the lock. the lock is a plain dbus `Inhibit` call (returns a unix fd) and is best-effort. +- the signalfd handles `SIGTERM`/`SIGINT` and runs pending resume commands before then exit, and also `SIGUSR1` forces the idle state immediately (like swayidle). + +idle detection via `get_idle_notification` also respects Wayland-level idle inhibitors (`zwp_idle_inhibitor_v1`), exactly like swayidle's default. + +## building + +requires `libwayland-dev`, `libdbus-1-dev`, `wayland-protocols` and `wayland-scanner` + +```sh +make +doas make install # will install to /usr/local/bin by default, change PREFIX if you want +``` + +you can also make the manpages (this requires scdoc): + +```sh +make man +doas make man-install +``` diff --git a/idle.c b/idle.c new file mode 100644 index 0000000000000000000000000000000000000000..0677010f4f75a9aadf05219ceea2be6c4fa56c12 --- /dev/null +++ b/idle.c @@ -0,0 +1,204 @@ +#include "swayaway.h" + +#include +#include +#include +#include + +int64_t mono_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +void idle_add_timeout(int seconds, char *idle_cmd, char *resume_cmd) +{ + struct timeout_cmd *tc = calloc(1, sizeof(*tc)); + if (!tc) { + sway_log("out of memory registering timeout"); + exit(1); + } + tc->seconds = seconds; + tc->idle_cmd = idle_cmd; + tc->resume_cmd = resume_cmd; + tc->fired = false; + tc->next = NULL; + + // append so commands run in configuration order + struct timeout_cmd **p = &state.timeouts; + while (*p) { + p = &(*p)->next; + } + *p = tc; + sway_log_debug("registered timeout %ds%s", seconds, + resume_cmd ? " with resume command" : ""); +} + +void idle_add_resume_cmd(char *cmd) +{ + struct resume_cmd *rc = calloc(1, sizeof(*rc)); + if (!rc) { + sway_log("out of memory registering resume command"); + exit(1); + } + rc->cmd = cmd; + rc->next = NULL; + struct resume_cmd **p = &state.resume_cmds; + while (*p) { + p = &(*p)->next; + } + *p = rc; +} + +bool idle_has_work(void) +{ + return state.timeouts != NULL || state.resume_cmds != NULL || + state.before_sleep_cmd || state.after_resume_cmd; +} + +// inhibited = one or more ScreenSaver cookies are held +void idle_recompute_inhibited(void) +{ + bool new_val = state.screensaver_inhibited; + if (new_val == state.inhibited) { + return; + } + state.inhibited = new_val; + if (new_val) { + sway_log("idle timeouts suppressed by inhibitor"); + // don't accumulate idle time while inhibited + } else { + sway_log("inhibitors released, resetting idle clock"); + /* swayidle restarts idle counting when an inhibitor goes away; + * copy that by resetting the clock and clearing fired flags */ + state.idle_since_ms = mono_ms(); + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + tc->fired = false; + } + idle_check_timeouts(); + } +} + +void idle_on_idled(void) +{ + sway_log_debug("compositor reports idle"); + state.is_idle = true; + state.idle_since_ms = mono_ms(); + idle_check_timeouts(); +} + +void idle_on_resumed(void) +{ + sway_log_debug("compositor reports activity"); + state.is_idle = false; + screensaver_emit_active(); + + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + if (tc->fired && tc->resume_cmd) { + cmd_exec(tc->resume_cmd); + } + tc->fired = false; + } + struct resume_cmd *rc; + for (rc = state.resume_cmds; rc; rc = rc->next) { + cmd_exec(rc->cmd); + } +} + +/* fire every timeout whose deadline has passed; called after every poll + * iteration and whenever idle/inhibit state changes */ +void idle_check_timeouts(void) +{ + if (!state.is_idle || state.inhibited) { + return; + } + int64_t now = mono_ms(); + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + if (tc->fired || tc->seconds <= 0) { + continue; + } + if (now - state.idle_since_ms >= (int64_t)tc->seconds * 1000) { + sway_log_debug("timeout %ds fired", tc->seconds); + tc->fired = true; + if (tc->idle_cmd) { + cmd_exec(tc->idle_cmd); + } + } + } + screensaver_emit_active(); // may transition active -> true +} + +/* the screensaver is "active" once at least one timeout command has fired + * during the current idle period (mirroring XScreenSaver's GetActive) */ +bool idle_any_timeout_fired(void) +{ + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + if (tc->fired) { + return true; + } + } + return false; +} + +/* milliseconds until the next timeout should fire, or -1 if nothing is + * pending; used as the poll() timeout so it wakes up exactly on time */ +int idle_next_timeout_ms(void) +{ + if (!state.is_idle || state.inhibited) { + return -1; + } + int64_t now = mono_ms(); + int64_t best = -1; + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + if (tc->fired || tc->seconds <= 0) { + continue; + } + int64_t deadline = state.idle_since_ms + (int64_t)tc->seconds * 1000; + int64_t remaining = deadline - now; + if (remaining < 0) { + remaining = 0; + } + if (best < 0 || remaining < best) { + best = remaining; + } + } + if (best < 0) { + return -1; + } + if (best > INT32_MAX) { + return INT32_MAX; + } + return (int)best; +} + +void idle_force_idle(void) +{ + sway_log("SIGUSR1: forcing idle state"); + if (state.inhibited) { + sway_log_debug("idle timeouts suppressed by inhibitor, not forcing"); + return; + } + state.is_idle = true; + state.idle_since_ms = mono_ms(); + struct timeout_cmd *tc; + for (tc = state.timeouts; tc; tc = tc->next) { + tc->fired = false; + } + idle_check_timeouts(); +} + +// SIGINT/SIGTERM: run pending resume commands, then terminate +void idle_terminate(void) +{ + sway_log("terminating"); + if (state.is_idle) { + idle_on_resumed(); + } + state.running = false; +} diff --git a/log.c b/log.c new file mode 100644 index 0000000000000000000000000000000000000000..c25081619e835d439b77e629ee9ecfdc34f1d623 --- /dev/null +++ b/log.c @@ -0,0 +1,132 @@ +#include "swayaway.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool debug_enabled = false; + +void log_init(bool debug) +{ + debug_enabled = debug; +} + +void sway_log(const char *fmt, ...) +{ + time_t t = time(NULL); + struct tm tm_buf; + struct tm *tm_info = localtime_r(&t, &tm_buf); + char ts[32]; + if (tm_info) { + strftime(ts, sizeof(ts), "%F %T", tm_info); + } else { + strcpy(ts, "??????????"); + } + + va_list args; + va_start(args, fmt); + fprintf(stderr, "%s - ", ts); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +void sway_log_debug(const char *fmt, ...) +{ + if (!debug_enabled) { + return; + } + time_t t = time(NULL); + struct tm tm_buf; + struct tm *tm_info = localtime_r(&t, &tm_buf); + char ts[32]; + if (tm_info) { + strftime(ts, sizeof(ts), "%F %T", tm_info); + } else { + strcpy(ts, "??????????"); + } + va_list args; + va_start(args, fmt); + fprintf(stderr, "%s - [debug] ", ts); + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + va_end(args); +} + +/* prepare the current process to run a user command: unblock signals that + * were blocked for signalfd, restore default dispositions, and drop every fd + * inherited from the daemon so long-running commands don't hold sockets + * open. */ +static void child_prepare_exec(void) +{ + sigset_t set; + sigemptyset(&set); + sigprocmask(SIG_SETMASK, &set, NULL); + signal(SIGINT, SIG_DFL); + signal(SIGTERM, SIG_DFL); + signal(SIGUSR1, SIG_DFL); + + long max = sysconf(_SC_OPEN_MAX); + if (max < 0 || max > 4096) { + max = 4096; + } + for (long fd = 3; fd < max; fd++) { + close((int)fd); + } +} + +/* run `cmd` through the shell ("sh -c") + * with state.wait, double-fork and block until the command exits; otherwise + * double-fork and return immediately, leaving the command running detached */ +void cmd_exec(const char *cmd) +{ + if (!cmd) { + return; + } + sway_log_debug("exec: %s", cmd); + + pid_t pid = fork(); + if (pid == 0) { + if (!state.wait) { + // intermediate child: fork the grandchild and exit + pid = fork(); + if (pid > 0) { + _exit(0); + } + } + if (pid == 0) { + child_prepare_exec(); + char *const argv[] = { "sh", "-c", (char *)cmd, NULL }; + execvp(argv[0], argv); + sway_log("exec failed: %s", strerror(errno)); + _exit(1); + } + _exit(0); + } else if (pid < 0) { + sway_log("fork failed: %s", strerror(errno)); + return; + } + + if (state.wait) { + int status; + sway_log_debug("waiting for command to finish"); + while (waitpid(pid, &status, 0) < 0) { + if (errno != EINTR) { + break; + } + } + if (WIFEXITED(status)) { + sway_log_debug("command exited: %d", WEXITSTATUS(status)); + } + } + // non-wait: reap the intermediate child + else { + waitpid(pid, NULL, 0); + } +} diff --git a/logind.c b/logind.c new file mode 100644 index 0000000000000000000000000000000000000000..40fc5e8ca22a90effff01650878a1b395373ae0b --- /dev/null +++ b/logind.c @@ -0,0 +1,163 @@ +/* this is deliberately the ONLY thing swayaway does on the system bus: + * it listens for org.freedesktop.login1.Manager.PrepareForSleep so that + * `before-sleep` and `after-resume` commands can be run around suspend + * it is implemented with plain libdbus-1 method calls and signal matches, + * so there is no elogind, no libsystemd, and no dependency on logind + * actually running. (if there is no logind, these events simply never fire) */ + +#include "swayaway.h" + +#include +#include +#include +#include +#include +#include +#include + +#define LOGIND_SERVICE "org.freedesktop.login1" +#define LOGIND_PATH "/org/freedesktop/login1" +#define LOGIND_MANAGER "org.freedesktop.login1.Manager" + +static void handle_prepare_for_sleep(DBusConnection *conn, DBusMessage *msg) +{ + (void)conn; + dbus_bool_t going_down = TRUE; + DBusError err; + dbus_error_init(&err); + + if (!dbus_message_get_args(msg, &err, DBUS_TYPE_BOOLEAN, &going_down, + DBUS_TYPE_INVALID)) { + sway_log_debug("bad PrepareForSleep: %s", + err.message ? err.message : "unknown error"); + dbus_error_free(&err); + return; + } + sway_log_debug("PrepareForSleep %s", going_down ? "going down" : "resumed"); + + if (going_down) { + if (state.before_sleep_cmd) { + cmd_exec(state.before_sleep_cmd); + } + // releasing the delay lock lets logind actually suspend now + logind_release_sleep_lock(); + } else { + if (state.after_resume_cmd) { + cmd_exec(state.after_resume_cmd); + } + logind_acquire_sleep_lock(); + } +} + +static DBusHandlerResult filter(DBusConnection *conn, DBusMessage *msg, + void *ud) +{ + (void)ud; + if (dbus_message_is_signal(msg, LOGIND_MANAGER, "PrepareForSleep")) { + handle_prepare_for_sleep(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +void logind_init(void) +{ + DBusError err; + dbus_error_init(&err); + + state.system_conn = dbus_bus_get(DBUS_BUS_SYSTEM, &err); + if (state.system_conn == NULL) { + sway_log("could not connect to the system bus: %s", + err.message ? err.message : "unknown error"); + dbus_error_free(&err); + return; + } + dbus_error_free(&err); + dbus_connection_set_exit_on_disconnect(state.system_conn, FALSE); + dbus_connection_get_unix_fd(state.system_conn, &state.system_fd); + + dbus_connection_add_filter(state.system_conn, filter, NULL, NULL); + + DBusError match_err; + dbus_error_init(&match_err); + dbus_bus_add_match(state.system_conn, + "type='signal',interface='org.freedesktop.login1.Manager'," + "member='PrepareForSleep',path='/org/freedesktop/login1'", + &match_err); + if (dbus_error_is_set(&match_err)) { + sway_log("could not add PrepareForSleep match: %s", match_err.message); + } + dbus_error_free(&match_err); + + logind_acquire_sleep_lock(); +} + +/* hold a logind "sleep" delay inhibitor so logind waits for the before-sleep + * command to finish + * + * inhibit returns a unix fd that keeps the lock alive until closed + * without logind this fails harmlessly and sleep events simply won't fire. */ +void logind_acquire_sleep_lock(void) +{ + if (state.system_conn == NULL || state.sleep_lock_fd >= 0) { + return; + } + if (!state.before_sleep_cmd && !state.after_resume_cmd) { + return; + } + + DBusMessage *call = dbus_message_new_method_call(LOGIND_SERVICE, + LOGIND_PATH, LOGIND_MANAGER, "Inhibit"); + if (!call) { + return; + } + const char *type = "sleep"; + const char *who = "swayaway"; + const char *why = "Swayaway is preventing sleep"; + const char *mode = "delay"; + dbus_message_append_args(call, DBUS_TYPE_STRING, &type, + DBUS_TYPE_STRING, &who, DBUS_TYPE_STRING, &why, + DBUS_TYPE_STRING, &mode, DBUS_TYPE_INVALID); + + DBusError err; + dbus_error_init(&err); + DBusMessage *reply = dbus_connection_send_with_reply_and_block( + state.system_conn, call, -1, &err); + dbus_message_unref(call); + if (reply == NULL) { + sway_log_debug("logind Inhibit failed: %s", + err.message ? err.message : "unknown error"); + dbus_error_free(&err); + return; + } + dbus_error_free(&err); + + int fd = -1; + if (!dbus_message_get_args(reply, &err, DBUS_TYPE_UNIX_FD, &fd, + DBUS_TYPE_INVALID)) { + sway_log_debug("logind Inhibit reply malformed: %s", + err.message ? err.message : "unknown error"); + dbus_error_free(&err); + dbus_message_unref(reply); + return; + } + dbus_message_unref(reply); + + int dup_fd = fcntl(fd, F_DUPFD_CLOEXEC, 3); + close(fd); + if (dup_fd >= 0) { + state.sleep_lock_fd = dup_fd; + sway_log_debug("holding logind sleep delay lock (fd %d)", dup_fd); + } else { + sway_log("could not duplicate sleep lock fd: %s", strerror(errno)); + } +} + +void logind_release_sleep_lock(void) +{ + if (state.sleep_lock_fd >= 0) { + sway_log_debug("releasing logind sleep delay lock"); + close(state.sleep_lock_fd); + state.sleep_lock_fd = -1; + } +} diff --git a/main.c b/main.c new file mode 100644 index 0000000000000000000000000000000000000000..74f28394abd6a3d52adbdbcf0e48ad403af05bc7 --- /dev/null +++ b/main.c @@ -0,0 +1,382 @@ +#include "swayaway.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct swayaway state; + +static void usage(const char *prog) +{ + printf("Usage: %s [options] [events...]\n", prog); + printf(" -C \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 [resume ]"); + 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 "); + 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 "); + 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 "); + 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; +} diff --git a/screensaver.c b/screensaver.c new file mode 100644 index 0000000000000000000000000000000000000000..5b11819e94edbea4c93f26d4b06f4147923a196c --- /dev/null +++ b/screensaver.c @@ -0,0 +1,338 @@ +#include "swayaway.h" + +#include +#include +#include +#include + +#define SS_PATH "/org/freedesktop/ScreenSaver" +#define SS_IFACE "org.freedesktop.ScreenSaver" +#define GNOME_PATH "/org/gnome/screen_saver" +#define GNOME_IFACE "org.gnome.ScreenSaver" + +// this sucks, yes, but who are you to judge +static const char introspect_xml[] = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; + +/* last emitted ActiveChanged value; emit only on real transitions to avoid + * flooding the bus with a signal per keystroke gap */ +static bool ss_active = false; + +static void recompute(void) +{ + state.screensaver_inhibited = (state.inhibitors != NULL); + idle_recompute_inhibited(); +} + +static void remove_inhibitor(struct inhibitor *inh) +{ + struct inhibitor **p = &state.inhibitors; + while (*p && *p != inh) { + p = &(*p)->next; + } + if (*p == inh) { + *p = inh->next; + } + free(inh->sender); + free(inh->app); + free(inh->reason); + free(inh); +} + +static struct inhibitor *find_cookie(uint32_t cookie) +{ + struct inhibitor *inh; + for (inh = state.inhibitors; inh; inh = inh->next) { + if (inh->cookie == cookie) { + return inh; + } + } + return NULL; +} + +static void reply_simple(DBusConnection *conn, DBusMessage *msg, + int type, void *value) +{ + DBusMessage *reply = dbus_message_new_method_return(msg); + if (!reply) { + return; + } + dbus_message_append_args(reply, type, value, DBUS_TYPE_INVALID); + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + dbus_connection_flush(conn); +} + +static void reply_error(DBusConnection *conn, DBusMessage *msg, + const char *text) +{ + DBusMessage *reply = dbus_message_new_error( + msg, DBUS_ERROR_INVALID_ARGS, text); + if (reply) { + dbus_connection_send(conn, reply, NULL); + dbus_message_unref(reply); + dbus_connection_flush(conn); + } +} + +static void handle_inhibit(DBusConnection *conn, DBusMessage *msg) +{ + const char *app = NULL; + const char *reason = NULL; + DBusError err; + dbus_error_init(&err); + + if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &app, + DBUS_TYPE_STRING, &reason, DBUS_TYPE_INVALID)) { + reply_error(conn, msg, err.message ? err.message : "invalid arguments"); + dbus_error_free(&err); + return; + } + + uint32_t cookie = ++state.next_cookie; + if (cookie == 0) { + cookie = ++state.next_cookie; + } + + struct inhibitor *inh = calloc(1, sizeof(*inh)); + if (!inh) { + reply_error(conn, msg, "out of memory"); + return; + } + inh->cookie = cookie; + const char *sender = dbus_message_get_sender(msg); + inh->sender = sender ? strdup(sender) : strdup(":unknown"); + inh->app = strdup(app ? app : ""); + inh->reason = strdup(reason ? reason : ""); + inh->next = state.inhibitors; + state.inhibitors = inh; + + sway_log("inhibit requested by %s (%s): %s", + inh->sender, inh->app, inh->reason); + recompute(); + + reply_simple(conn, msg, DBUS_TYPE_UINT32, &cookie); +} + +static void handle_uninhibit(DBusConnection *conn, DBusMessage *msg) +{ + uint32_t cookie = 0; + DBusError err; + dbus_error_init(&err); + + if (!dbus_message_get_args(msg, &err, DBUS_TYPE_UINT32, &cookie, + DBUS_TYPE_INVALID)) { + reply_error(conn, msg, err.message ? err.message : "invalid arguments"); + dbus_error_free(&err); + return; + } + + struct inhibitor *inh = find_cookie(cookie); + if (inh) { + sway_log("uninhibit cookie %u", cookie); + remove_inhibitor(inh); + recompute(); + } else { + sway_log_debug("uninhibit for unknown cookie %u", cookie); + } + reply_simple(conn, msg, DBUS_TYPE_INVALID, NULL); +} + +static void handle_get_active(DBusConnection *conn, DBusMessage *msg) +{ + dbus_bool_t active = idle_any_timeout_fired(); + reply_simple(conn, msg, DBUS_TYPE_BOOLEAN, &active); +} + +static void handle_get_active_time(DBusConnection *conn, DBusMessage *msg) +{ + uint32_t secs = 0; + if (idle_any_timeout_fired() && state.is_idle) { + secs = (uint32_t)((mono_ms() - state.idle_since_ms) / 1000); + } + reply_simple(conn, msg, DBUS_TYPE_UINT32, &secs); +} + +static void handle_name_owner_changed(DBusConnection *conn, DBusMessage *msg) +{ + (void)conn; + const char *name = NULL, *old_owner = NULL, *new_owner = NULL; + DBusError err; + dbus_error_init(&err); + + if (!dbus_message_get_args(msg, &err, DBUS_TYPE_STRING, &name, + DBUS_TYPE_STRING, &old_owner, DBUS_TYPE_STRING, &new_owner, + DBUS_TYPE_INVALID)) { + dbus_error_free(&err); + return; + } + + // a connection died (new_owner empty): release everything it held + if (name && *name && new_owner && *new_owner == '\0') { + struct inhibitor *inh = state.inhibitors; + while (inh) { + struct inhibitor *next = inh->next; + if (strcmp(inh->sender, name) == 0) { + sway_log("client %s disconnected, releasing its inhibitors", + name); + remove_inhibitor(inh); + } + inh = next; + } + recompute(); + } +} + +static DBusHandlerResult filter(DBusConnection *conn, DBusMessage *msg, + void *ud) +{ + (void)ud; + + if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", + "Introspect")) { + const char *xml = introspect_xml; + reply_simple(conn, msg, DBUS_TYPE_STRING, &xml); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_method_call(msg, SS_IFACE, "Inhibit") || + dbus_message_is_method_call(msg, GNOME_IFACE, "Inhibit")) { + handle_inhibit(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + if (dbus_message_is_method_call(msg, SS_IFACE, "Uninhibit") || + dbus_message_is_method_call(msg, GNOME_IFACE, "Uninhibit")) { + handle_uninhibit(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + if (dbus_message_is_method_call(msg, SS_IFACE, "GetActive")) { + handle_get_active(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + if (dbus_message_is_method_call(msg, SS_IFACE, "GetActiveTime")) { + handle_get_active_time(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + + if (dbus_message_is_signal(msg, DBUS_INTERFACE_DBUS, "NameOwnerChanged")) { + handle_name_owner_changed(conn, msg); + return DBUS_HANDLER_RESULT_HANDLED; + } + + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; +} + +/* Emit ActiveChanged only when the "screensaver active" state (at least one + * timeout command fired) actually transitions. */ +void screensaver_emit_active(void) +{ + if (!state.session_conn) { + return; + } + bool now = idle_any_timeout_fired(); + if (now == ss_active) { + return; + } + ss_active = now; + + DBusMessage *sig = dbus_message_new_signal(SS_PATH, SS_IFACE, + "ActiveChanged"); + if (!sig) { + return; + } + dbus_bool_t active = now; + dbus_message_append_args(sig, DBUS_TYPE_BOOLEAN, &active, + DBUS_TYPE_INVALID); + dbus_connection_send(state.session_conn, sig, NULL); + dbus_message_unref(sig); + dbus_connection_flush(state.session_conn); +} + +static void request_name(DBusConnection *conn, const char *name) +{ + DBusError err; + dbus_error_init(&err); + int ret = dbus_bus_request_name(conn, name, + DBUS_NAME_FLAG_DO_NOT_QUEUE, &err); + if (ret == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) { + sway_log("exporting %s", name); + } else { + sway_log("could not acquire %s (%s); inhibitor service degraded", + name, err.message ? err.message : "unknown error"); + } + dbus_error_free(&err); +} + +void screensaver_init(void) +{ + DBusError err; + dbus_error_init(&err); + + state.session_conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (state.session_conn == NULL) { + sway_log("could not connect to the session bus: %s", + err.message ? err.message : "unknown error"); + dbus_error_free(&err); + return; + } + dbus_error_free(&err); + dbus_connection_set_exit_on_disconnect(state.session_conn, FALSE); + dbus_connection_get_unix_fd(state.session_conn, &state.session_fd); + + dbus_connection_add_filter(state.session_conn, filter, NULL, NULL); + + DBusError match_err; + dbus_error_init(&match_err); + dbus_bus_add_match(state.session_conn, + "type='signal',interface='org.freedesktop.DBus'," + "member='NameOwnerChanged',path='/org/freedesktop/DBus'", + &match_err); + if (dbus_error_is_set(&match_err)) { + sway_log("could not add NameOwnerChanged match: %s", + match_err.message); + } + dbus_error_free(&match_err); + + request_name(state.session_conn, SS_IFACE); + request_name(state.session_conn, GNOME_IFACE); +} + +bool screensaver_have_service(void) +{ + return state.session_conn != NULL; +} diff --git a/swayaway.1.scd b/swayaway.1.scd new file mode 100644 index 0000000000000000000000000000000000000000..605faae3a985c611d4e4e26425a1e5d444799cb2 --- /dev/null +++ b/swayaway.1.scd @@ -0,0 +1,99 @@ +swayaway(1) + +# NAME + +swayaway - idle manager for Wayland + +# SYNOPSIS + +*swayaway* [options] [events...] + +# OPTIONS + +*-C* + path to the config file. by default, the following paths are checked + in order: + + . $XDG_CONFIG_HOME/swayaway/config + . $HOME/.config/swayaway/config + . $HOME/.swayaway/config + . $XDG_CONFIG_HOME/swayidle/config + . $HOME/.config/swayidle/config + . $HOME/.swayidle/config + + so an existing swayidle config works unchanged. config lines are the same events as the command line; _#_ starts a comment and shell expansion applies (quote commands with spaces: timeout 300 "swaylock -f"). specifying events in the config and as arguments is not mutually exclusive. + +*-d* + debug output. + +*-w* + wait for commands to finish before continuing. useful with *before-sleep* and swaylock *-f*, so swaylock has actually locked the screen before the system goes to sleep. note that this blocks until the command finishes. + +*-S* + pick a specific seat. by default, if no name is specified, an arbitrary seat is picked instead. + +*-h* + show help message and quit. + +# DESCRIPTION + +swayaway is a drop-in replacement for swayidle(1). it listens for idle activity on your Wayland compositor and executes tasks on various idle-related events. you can specify any number of events at the command line and in the config file. + +the only dependencies are libwayland-client and libdbus-1; there is no elogind dependency and no libsystemd. + +it exports _org.freedesktop.ScreenSaver_ (plus the _org.gnome.ScreenSaver_ alias) on the session bus, so browsers, media players, and any other app that calls Inhibit()/Uninhibit() can suppress idle timeouts. + +it also listens for logind's _PrepareForSleep_ signal to run *before-sleep* / *after-resume* commands. if no logind is running, these events simply never fire. + +idle detection also respects Wayland-level idle inhibitors (_zwp_idle_inhibitor_v1_), exactly like swayidle's default. + +# EVENTS + +*timeout* [resume ] + execute _timeout command_ if there is no activity for seconds. if you specify "resume ", _resume command_ + will be run when there is activity again. this is the swayidle 1.8+/1.9 syntax. + + the legacy mode syntax `timeout N resume CMD` (pre-1.8) is also accepted; the command then runs on timeout *and* on resume. + +*resume* + legacy top-level event; runs _command_ on every activity. + +*before-sleep* + executes _command_ when logind signals that the computer is about to sleep, before the sleep is allowed to proceed. + + note: this only delays sleeping up to the limit set in *logind.conf*(5) by the option InhibitDelayMaxSec. a command that has not finished by then will continue running after resuming from sleep. + +*after-resume* + executes _command_ after logind signals that the computer resumed from sleep. + +swayidle's *lock*, *unlock* and *idlehint* events go through the logind Session interface and are *not* supported; swayaway rejects them with an error, same as swayidle built without systemd/elogind. + +all commands are executed in a shell. + +# SIGNALS + +swayaway responds to the following signals: + +*SIGTERM, SIGINT* + run all pending resume commands. when finished, swayaway terminates. + +*SIGUSR1* + immediately enter idle state. + +# EXAMPLE + +``` +swayaway -w \\ + timeout 300 'swaylock -f -c 000000' \\ + timeout 600 'swaymsg "output * power off"' \\ + resume 'swaymsg "output * power on"' \\ + before-sleep 'swaylock -f -c 000000' +``` + +this will lock your screen after 300 seconds of inactivity, then turn off your displays after another 300 seconds, and turn your screens back on when resumed. it will also lock your screen before your computer goes to sleep. + +to make sure swayaway waits for swaylock to lock the screen before it releases the logind sleep lock, the *-w* option is used in swayaway, and *-f* in swaylock. + +# SEE ALSO + +*sway*(5) *swaymsg*(1) *sway-input*(5) *sway-output*(5) *sway-bar*(5) *loginctl*(1) diff --git a/swayaway.h b/swayaway.h new file mode 100644 index 0000000000000000000000000000000000000000..99e61ab58461939feba047ffcdff137d220d9bb5 --- /dev/null +++ b/swayaway.h @@ -0,0 +1,128 @@ +/* swayaway.h - shared state and declarations for swayaway + * + * swayaway is a drop-in replacement for swayidle(1) that depends ONLY on + * libwayland-client and libdbus-1. No elogind, no libsystemd. + * + * Idle timeout handling is driven by a monotonic clock (CLOCK_MONOTONIC) + * fed by a single ext-idle-notify-v1 tracking notification, and idle + * inhibition is understood natively by exporting org.freedesktop.ScreenSaver + * on the session bus. + * + * Everything runs in one process on a single poll() loop watching: + * - the wayland fd (wl_display_get_fd) + * - the session bus fd (the inhibitor service) + * - the system bus fd (logind PrepareForSleep sleep events) + * - a signalfd (SIGINT/SIGTERM/SIGUSR1) + */ +#ifndef SWAYAWAY_H +#define SWAYAWAY_H + +#define _GNU_SOURCE +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +struct wl_display; +struct wl_registry; +struct wl_seat; +struct ext_idle_notifier_v1; + +struct timeout_cmd { + struct timeout_cmd *next; + int seconds; // idle timeout in seconds (>0), -1 = disabled + bool fired; // already fired during the current idle cycle + char *idle_cmd; // shell command run on timeout, may be NULL + char *resume_cmd; // shell command run on resume, may be NULL +}; + +// legacy top-level `resume ` event +struct resume_cmd { + struct resume_cmd *next; + char *cmd; +}; + +// one held org.freedesktop.ScreenSaver cookie +struct inhibitor { + struct inhibitor *next; + uint32_t cookie; + char *sender; // unique bus name, e.g. ":1.42" + char *app; + char *reason; +}; + +struct swayaway { + // options + bool wait; + bool debug; + char *seat_name; + + // wayland + struct wl_display *display; + struct ext_idle_notifier_v1 *notifier; + struct wl_seat *seat; + + // idle state + struct timeout_cmd *timeouts; + struct resume_cmd *resume_cmds; + bool is_idle; // compositor reported idle + int64_t idle_since_ms; // CLOCK_MONOTONIC at idle start + bool inhibited; // any inhibitor active + bool screensaver_inhibited; // someone holds a ScreenSaver lock + + // session bus / inhibitor service + DBusConnection *session_conn; + int session_fd; + uint32_t next_cookie; + struct inhibitor *inhibitors; + + // system bus / sleep events + DBusConnection *system_conn; + int system_fd; + int sleep_lock_fd; // held logind sleep delay lock + + // sleep events + char *before_sleep_cmd; + char *after_resume_cmd; + + bool running; +}; + +extern struct swayaway state; + +// log.c +void log_init(bool debug); +void sway_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +void sway_log_debug(const char *fmt, ...) __attribute__((format(printf, 1, 2))); +void cmd_exec(const char *cmd); + +// idle.c +void idle_add_timeout(int seconds, char *idle_cmd, char *resume_cmd); +void idle_add_resume_cmd(char *cmd); +void idle_on_idled(void); +void idle_on_resumed(void); +void idle_check_timeouts(void); +int idle_next_timeout_ms(void); +void idle_force_idle(void); +void idle_terminate(void); +void idle_recompute_inhibited(void); +bool idle_has_work(void); +bool idle_any_timeout_fired(void); +int64_t mono_ms(void); + +// wayland.c +int wayland_init(void); +void wayland_create_tracking_notification(void); + +// screensaver.c +void screensaver_init(void); +bool screensaver_have_service(void); +void screensaver_emit_active(void); + +// logind.c +void logind_init(void); +void logind_acquire_sleep_lock(void); +void logind_release_sleep_lock(void); + +#endif /* SWAYAWAY_H */ diff --git a/wayland.c b/wayland.c new file mode 100644 index 0000000000000000000000000000000000000000..c8ebc41a8fcb18745feda433a0f35c56bb62fa12 --- /dev/null +++ b/wayland.c @@ -0,0 +1,154 @@ +#include "swayaway.h" + +#include +#include +#include +#include +#include +#include "ext-idle-notify-v1-client-protocol.h" + +struct seat_entry { + struct seat_entry *next; + struct wl_seat *proxy; + char *name; +}; + +static struct seat_entry *seats; +static struct ext_idle_notification_v1 *tracking; + +/* One tracking notification with timeout 0 is enough: the compositor sends + * "idled" the moment the seat goes idle (a zero timeout means "as soon as + * possible") and "resumed" when activity starts again, then restarts the + * timer. All real timeout bookkeeping happens in idle.c against + * CLOCK_MONOTONIC, so one protocol object per timeout is not needed. */ + +static void handle_idled(void *data, struct ext_idle_notification_v1 *notif) +{ + (void)data; + (void)notif; + idle_on_idled(); +} + +static void handle_resumed(void *data, struct ext_idle_notification_v1 *notif) +{ + (void)data; + (void)notif; + idle_on_resumed(); +} + +static const struct ext_idle_notification_v1_listener tracking_listener = { + .idled = handle_idled, + .resumed = handle_resumed, +}; + +static void seat_handle_capabilities(void *data, struct wl_seat *seat, + uint32_t capabilities) +{ + (void)data; + (void)seat; + (void)capabilities; +} + +static void seat_handle_name(void *data, struct wl_seat *seat, + const char *name) +{ + struct seat_entry *entry = data; + (void)seat; + if (name) { + entry->name = strdup(name); + } +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +static void handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) +{ + (void)data; + if (strcmp(interface, ext_idle_notifier_v1_interface.name) == 0) { + state.notifier = wl_registry_bind(registry, name, + &ext_idle_notifier_v1_interface, 1); + sway_log_debug("bound ext_idle_notifier_v1 (version %u)", version); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + struct seat_entry *entry = calloc(1, sizeof(*entry)); + if (!entry) { + sway_log("out of memory binding seat"); + return; + } + entry->proxy = wl_registry_bind(registry, name, &wl_seat_interface, 2); + wl_seat_add_listener(entry->proxy, &seat_listener, entry); + entry->next = seats; + seats = entry; + sway_log_debug("bound wl_seat"); + } +} + +static void handle_global_remove(void *data, struct wl_registry *registry, + uint32_t name) +{ + (void)data; + (void)registry; + (void)name; +} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + +int wayland_init(void) +{ + state.display = wl_display_connect(NULL); + if (state.display == NULL) { + sway_log("unable to connect to the compositor. " + "Check or set WAYLAND_DISPLAY."); + return -1; + } + + struct wl_registry *registry = wl_display_get_registry(state.display); + wl_registry_add_listener(registry, ®istry_listener, NULL); + // roundtrip twice: once for globals, once for the seat name + wl_display_roundtrip(state.display); + wl_display_roundtrip(state.display); + + if (state.notifier == NULL) { + sway_log("compositor does not support ext-idle-notify-v1"); + return -1; + } + + struct seat_entry *entry; + for (entry = seats; entry; entry = entry->next) { + if (state.seat_name == NULL || + (entry->name && strcmp(entry->name, state.seat_name) == 0)) { + state.seat = entry->proxy; + break; + } + } + + if (state.seat == NULL) { + if (state.seat_name) { + sway_log("seat '%s' not found", state.seat_name); + } else { + sway_log("no seat found"); + } + return -1; + } + sway_log_debug("using seat '%s'", + entry->name ? entry->name : "?"); + return 0; +} + +void wayland_create_tracking_notification(void) +{ + if (state.notifier == NULL || state.seat == NULL || tracking != NULL) { + return; + } + tracking = ext_idle_notifier_v1_get_idle_notification( + state.notifier, 0, state.seat); + ext_idle_notification_v1_add_listener(tracking, &tracking_listener, NULL); + sway_log_debug("created tracking idle notification (timeout 0)"); + wl_display_flush(state.display); +}