diff --git a/CHANGELOG.md b/CHANGELOG.md index e42ef36a..2d8bfc38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added launch arguments to manage launchd service: `--install-service`, `--uninstall-service`, `--start-service`, `--restart-service`, `--stop-service` [#1619](https://github.com/koekeishiya/yabai/issues/1619) ## [5.0.3] - 2023-03-28 ### Changed diff --git a/doc/yabai.1 b/doc/yabai.1 index 3c70c9d5..f6fdf5a4 100644 --- a/doc/yabai.1 +++ b/doc/yabai.1 @@ -2,12 +2,12 @@ .\" Title: yabai .\" Author: [see the "AUTHOR(S)" section] .\" Generator: Asciidoctor 2.0.16 -.\" Date: 2022-12-28 +.\" Date: 2023-04-14 .\" Manual: Yabai Manual .\" Source: Yabai .\" Language: English .\" -.TH "YABAI" "1" "2022-12-28" "Yabai" "Yabai Manual" +.TH "YABAI" "1" "2023-04-14" "Yabai" "Yabai Manual" .ie \n(.g .ds Aq \(aq .el .ds Aq ' .ss \n[.ss] 0 @@ -32,7 +32,7 @@ yabai .SH "SYNOPSIS" .sp -\fByabai\fP [\fB\-v\fP,\fB\-\-version\fP|\fB\-V\fP,\fB\-\-verbose\fP|\fB\-m\fP,\fB\-\-message\fP \fImsg\fP|\fB\-c\fP,\fB\-\-config\fP \fIconfig_file\fP|\fB\-\-uninstall\-sa\fP|\fB\-\-load\-sa\fP] +\fByabai\fP [\fB\-v\fP,\fB\-\-version\fP|\fB\-V\fP,\fB\-\-verbose\fP|\fB\-m\fP,\fB\-\-message\fP \fImsg\fP|\fB\-c\fP,\fB\-\-config\fP \fIconfig_file\fP|\fB\-\-uninstall\-sa\fP|\fB\-\-load\-sa\fP|\fB\-\-install\-service\fP|\fB\-\-uninstall\-service\fP|\fB\-\-start\-service\fP|\fB\-\-restart\-service\fP|\fB\-\-stop\-service\fP] .SH "DESCRIPTION" .sp \fByabai\fP is a tiling window manager for macOS based on binary space partitioning. diff --git a/doc/yabai.asciidoc b/doc/yabai.asciidoc index 4ff8fa50..934a4232 100644 --- a/doc/yabai.asciidoc +++ b/doc/yabai.asciidoc @@ -24,7 +24,7 @@ yabai Synopsis -------- -*yabai* [*-v*,*--version*|*-V*,*--verbose*|*-m*,*--message* 'msg'|*-c*,*--config* 'config_file'|*--uninstall-sa*|*--load-sa*] +*yabai* [*-v*,*--version*|*-V*,*--verbose*|*-m*,*--message* 'msg'|*-c*,*--config* 'config_file'|*--uninstall-sa*|*--load-sa*|*--install-service*|*--uninstall-service*|*--start-service*|*--restart-service*|*--stop-service*] Description ----------- diff --git a/src/manifest.m b/src/manifest.m index 613bf495..af847bbc 100644 --- a/src/manifest.m +++ b/src/manifest.m @@ -27,6 +27,7 @@ #include #include #include +#include #include "misc/extern.h" #include "misc/macros.h" @@ -40,6 +41,7 @@ #define HASHTABLE_IMPLEMENTATION #include "misc/hashtable.h" #undef HASHTABLE_IMPLEMENTATION +#include "misc/service.h" #include "osax/common.h" diff --git a/src/misc/service.h b/src/misc/service.h new file mode 100644 index 00000000..c2fabab8 --- /dev/null +++ b/src/misc/service.h @@ -0,0 +1,162 @@ +#ifndef SERVICE_H +#define SERVICE_H + +#define _NAME_YABAI_PLIST "com.koekeishiya.yabai" +#define _PATH_YABAI_PLIST "/Users/%s/Library/LaunchAgents/"_NAME_YABAI_PLIST".plist" +#define _PATH_LAUNCHCTL "/bin/launchctl" + +#define _YABAI_PLIST \ + "\n" \ + "\n" \ + "\n" \ + "\n" \ + " Label\n" \ + " "_NAME_YABAI_PLIST"\n" \ + " ProgramArguments\n" \ + " \n" \ + " %s\n" \ + " \n" \ + " EnvironmentVariables\n" \ + " \n" \ + " PATH\n" \ + " %s\n" \ + " \n" \ + " RunAtLoad\n" \ + " \n" \ + " KeepAlive\n" \ + " \n" \ + " StandardOutPath\n" \ + " /tmp/yabai.out.log\n" \ + " StandardErrorPath\n" \ + " /tmp/yabai.err.log\n" \ + " ThrottleInterval\n" \ + " 30\n" \ + " ProcessType\n" \ + " Interactive\n" \ + " Nice\n" \ + " -20\n" \ + "\n" \ + "" + +static int safe_exec(char *const argv[]) +{ + pid_t pid; + int status = posix_spawn(&pid, argv[0], NULL, NULL, argv, NULL); + if (status) return 1; + + while ((waitpid(pid, &status, 0) == -1) && (errno == EINTR)) { + usleep(1000); + } + + if (WIFSIGNALED(status)) { + return 1; + } else if (WIFSTOPPED(status)) { + return 1; + } else { + return WEXITSTATUS(status); + } +} + +static void populate_plist_path(char *yabai_plist_path, int size) +{ + char *user = getenv("USER"); + if (!user) { + error("yabai: 'env USER' not set! abort..\n"); + } + + snprintf(yabai_plist_path, size, _PATH_YABAI_PLIST, user); +} + +static void populate_plist(char *yabai_plist, int size) +{ + char *path_env = getenv("PATH"); + if (!path_env) { + error("yabai: 'env PATH' not set! abort..\n"); + } + + char exe_path[4096]; + unsigned int exe_path_size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &exe_path_size) < 0) { + error("yabai: unable to retrieve path of executable! abort..\n"); + } + + snprintf(yabai_plist, size, _YABAI_PLIST, exe_path, path_env); +} + +static int service_install(void) +{ + char yabai_plist_path[MAXLEN]; + populate_plist_path(yabai_plist_path, sizeof(yabai_plist_path)); + + if (file_exists(yabai_plist_path)) { + error("yabai: service file '%s' is already installed! abort..\n", yabai_plist_path); + } + + char yabai_plist[4096]; + populate_plist(yabai_plist, sizeof(yabai_plist)); + + FILE *handle = fopen(yabai_plist_path, "w"); + if (!handle) return 1; + + size_t bytes = fwrite(yabai_plist, strlen(yabai_plist), 1, handle); + int result = bytes == 1 ? 0 : 1; + fclose(handle); + + return result; +} + +static int service_uninstall(void) +{ + char yabai_plist_path[MAXLEN]; + populate_plist_path(yabai_plist_path, sizeof(yabai_plist_path)); + + if (!file_exists(yabai_plist_path)) { + error("yabai: service file '%s' is not installed! abort..\n", yabai_plist_path); + } + + return unlink(yabai_plist_path) == 0 ? 0 : 1; +} + +static int service_start(void) +{ + char yabai_plist_path[MAXLEN]; + populate_plist_path(yabai_plist_path, sizeof(yabai_plist_path)); + + if (!file_exists(yabai_plist_path)) { + error("yabai: service file '%s' is not installed! abort..\n", yabai_plist_path); + } + + const char *const args[] = { _PATH_LAUNCHCTL, "load", "-w", yabai_plist_path, NULL }; + return safe_exec((char *const*)args); +} + +static int service_restart(void) +{ + char yabai_plist_path[MAXLEN]; + populate_plist_path(yabai_plist_path, sizeof(yabai_plist_path)); + + if (!file_exists(yabai_plist_path)) { + error("yabai: service file '%s' is not installed! abort..\n", yabai_plist_path); + } + + char yabai_service_id[MAXLEN]; + snprintf(yabai_service_id, sizeof(yabai_service_id), "gui/%d/%s", getuid(), _NAME_YABAI_PLIST); + + const char *const args[] = { _PATH_LAUNCHCTL, "kickstart", "-k", yabai_service_id, NULL }; + return safe_exec((char *const*)args); +} + +static int service_stop(void) +{ + char yabai_plist_path[MAXLEN]; + populate_plist_path(yabai_plist_path, sizeof(yabai_plist_path)); + + if (!file_exists(yabai_plist_path)) { + error("yabai: service file '%s' is not installed! abort..\n", yabai_plist_path); + } + + const char *const args[] = { _PATH_LAUNCHCTL, "unload", "-w", yabai_plist_path, NULL }; + return safe_exec((char *const*)args); +} + +#endif diff --git a/src/yabai.c b/src/yabai.c index 26ea8254..e8d4c9c4 100644 --- a/src/yabai.c +++ b/src/yabai.c @@ -15,6 +15,12 @@ #define SCRPT_ADD_UNINSTALL_OPT "--uninstall-sa" #define SCRPT_ADD_LOAD_OPT "--load-sa" +#define SERVICE_INSTALL_OPT "--install-service" +#define SERVICE_UNINSTALL_OPT "--uninstall-service" +#define SERVICE_START_OPT "--start-service" +#define SERVICE_RESTART_OPT "--restart-service" +#define SERVICE_STOP_OPT "--stop-service" + #define MAJOR 5 #define MINOR 0 #define PATCH 3 @@ -246,6 +252,26 @@ static void parse_arguments(int argc, char **argv) exit(scripting_addition_load()); } + if (string_equals(argv[1], SERVICE_INSTALL_OPT)) { + exit(service_install()); + } + + if (string_equals(argv[1], SERVICE_UNINSTALL_OPT)) { + exit(service_uninstall()); + } + + if (string_equals(argv[1], SERVICE_START_OPT)) { + exit(service_start()); + } + + if (string_equals(argv[1], SERVICE_RESTART_OPT)) { + exit(service_restart()); + } + + if (string_equals(argv[1], SERVICE_STOP_OPT)) { + exit(service_stop()); + } + for (int i = 1; i < argc; ++i) { char *opt = argv[i];