pax_global_header00006660000000000000000000000064145567531360014531gustar00rootroot0000000000000052 comment=0e0ac738fc6f63c1022f7194619b41fa7e3886c5 kanshi-1.5.1/000077500000000000000000000000001455675313600130125ustar00rootroot00000000000000kanshi-1.5.1/.build.yml000066400000000000000000000006071455675313600147150ustar00rootroot00000000000000image: archlinux packages: - meson - wayland - scdoc - libvarlink sources: - https://git.sr.ht/~emersion/kanshi tasks: - setup: | cd kanshi meson setup build/ -Dauto_features=enabled - build: | cd kanshi ninja -C build/ - build-features-disabled: | cd kanshi meson setup build/ --reconfigure -Dauto_features=disabled ninja -C build/ kanshi-1.5.1/.gitignore000066400000000000000000000000151455675313600147760ustar00rootroot00000000000000/subprojects kanshi-1.5.1/LICENSE000066400000000000000000000020521455675313600140160ustar00rootroot00000000000000MIT License Copyright (c) 2019 Simon Ser 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 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. kanshi-1.5.1/README.md000066400000000000000000000027161455675313600142770ustar00rootroot00000000000000# [kanshi] kanshi allows you to define output profiles that are automatically enabled and disabled on hotplug. For instance, this can be used to turn a laptop's internal screen off when docked. This is a Wayland equivalent for tools like [autorandr]. kanshi can be used on Wayland compositors supporting the wlr-output-management protocol. Join the IRC channel: #emersion on Libera Chat. ## Building Dependencies: * wayland-client * scdoc (optional, for man pages) * libvarlink (optional, for remote control functionality) ```sh meson build ninja -C build ``` ## Usage ```sh mkdir -p ~/.config/kanshi && touch ~/.config/kanshi/config kanshi ``` ## Configuration file Each output profile is delimited by brackets. It contains several `output` directives (whose syntax is similar to `sway-output(5)`). A profile will be enabled if all of the listed outputs are connected. ``` profile { output LVDS-1 disable output "Some Company ASDF 4242" mode 1600x900 position 0,0 } profile { output LVDS-1 enable scale 2 } ``` ## Contributing The upstream repository can be found [on SourceHut][repo]. Open tickets [on the SourceHut tracker][issue-tracker], send patches [on the mailing list][mailing-list]. ## License MIT [kanshi]: https://wayland.emersion.fr/kanshi/ [autorandr]: https://github.com/phillipberndt/autorandr [repo]: https://git.sr.ht/~emersion/kanshi [issue-tracker]: https://todo.sr.ht/~emersion/kanshi [mailing-list]: https://lists.sr.ht/~emersion/public-inbox kanshi-1.5.1/ctl.c000066400000000000000000000064261455675313600137500ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include "ipc.h" static void usage(void) { fprintf(stderr, "Usage: kanshictl [command]\n" "\n" "Commands:\n" " reload Reload the configuration file\n" " switch Switch to another profile\n"); } static long handle_call_done(VarlinkConnection *connection, const char *error, VarlinkObject *parameters, uint64_t flags, void *userdata) { if (error != NULL) { if (strcmp(error, "fr.emersion.kanshi.ProfileNotFound") == 0) { fprintf(stderr, "Profile not found\n"); } else if (strcmp(error, "fr.emersion.kanshi.ProfileNotMatched") == 0) { fprintf(stderr, "Profile does not match the current output configuration\n"); } else if (strcmp(error, "fr.emersion.kanshi.ProfileNotApplied") == 0) { fprintf(stderr, "Profile could not be applied by the compositor\n"); } else { fprintf(stderr, "Error: %s\n", error); } exit(EXIT_FAILURE); } return varlink_connection_close(connection); } static int set_blocking(int fd) { int flags = fcntl(fd, F_GETFL); if (flags == -1) { fprintf(stderr, "fnctl F_GETFL failed: %s\n", strerror(errno)); return -1; } flags &= ~O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { fprintf(stderr, "fnctl F_SETFL failed: %s\n", strerror(errno)); return -1; } return 0; } static int wait_for_event(VarlinkConnection *connection) { int fd = varlink_connection_get_fd(connection); if (set_blocking(fd) != 0) { return -1; } while (!varlink_connection_is_closed(connection)) { uint32_t events = varlink_connection_get_events(connection); long result = varlink_connection_process_events(connection, events); if (result != 0) { fprintf(stderr, "varlink_connection_process_events failed: %s\n", varlink_error_string(-result)); return -1; } } return 0; } int main(int argc, char *argv[]) { if (argc < 2) { usage(); return EXIT_FAILURE; } if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) { usage(); return EXIT_SUCCESS; } VarlinkConnection *connection; char address[PATH_MAX]; if (get_ipc_address(address, sizeof(address)) < 0) { return EXIT_FAILURE; } if (varlink_connection_new(&connection, address) != 0) { fprintf(stderr, "Couldn't connect to kanshi at %s.\n" "Is the kanshi daemon running?\n", address); return EXIT_FAILURE; } const char *command = argv[1]; long ret; if (strcmp(command, "reload") == 0) { ret = varlink_connection_call(connection, "fr.emersion.kanshi.Reload", NULL, 0, handle_call_done, NULL); } else if (strcmp(command, "switch") == 0) { if (argc < 3) { usage(); return EXIT_FAILURE; } const char *profile = argv[2]; VarlinkObject *params = NULL; varlink_object_new(¶ms); varlink_object_set_string(params, "profile", profile); ret = varlink_connection_call(connection, "fr.emersion.kanshi.Switch", params, 0, handle_call_done, NULL); varlink_object_unref(params); } else { fprintf(stderr, "invalid command: %s\n", argv[1]); usage(); return EXIT_FAILURE; } if (ret != 0) { fprintf(stderr, "varlink_connection_call failed: %s\n", varlink_error_string(-ret)); return EXIT_FAILURE; } return wait_for_event(connection); } kanshi-1.5.1/doc/000077500000000000000000000000001455675313600135575ustar00rootroot00000000000000kanshi-1.5.1/doc/kanshi.1.scd000066400000000000000000000022121455675313600156630ustar00rootroot00000000000000kanshi(1) # NAME kanshi - dynamic output configuration # SYNOPSIS *kanshi* [options...] # OPTIONS *-h, --help* Show help message and quit. *-c, --config* Specifies a config file. *-l, --listen-fd* Listen on the specified file descriptor for IPC. # DESCRIPTION kanshi is a Wayland daemon that automatically configures outputs. kanshi is configured with a list of output profiles. Each profile contains a set of outputs. A profile will be automatically activated if all specified outputs are currently connected. A profile contains configuration for each output. If kanshi receives a SIGHUP signal, it will reread its config file. # CONFIGURATION kanshi reads its configuration from *$XDG_CONFIG_HOME/kanshi/config*. If unset, *$XDG_CONFIG_HOME* defaults to *~/.config*. An error is raised if no configuration file is found. For information on the configuration file format, see *kanshi*(5). # AUTHORS Maintained by Simon Ser , who is assisted by other open-source contributors. For more information about kanshi development, see . # SEE ALSO *kanshi*(5) *kanshictl*(1) kanshi-1.5.1/doc/kanshi.5.scd000066400000000000000000000063301455675313600156740ustar00rootroot00000000000000kanshi(5) # NAME kanshi - configuration file # DESCRIPTION A kanshi configuration file is a list of profiles. Each profile has an optional name and contains profile directives delimited by brackets (*{* and *}*). Example: ``` include /etc/kanshi/config.d/* profile { output LVDS-1 disable output "Some Company ASDF 4242" mode 1600x900 position 0,0 } profile nomad { output LVDS-1 enable scale 2 } ``` # DIRECTIVES *profile* [] { } Defines a new profile using the specified bracket-delimited profile directives. A name can be specified but is optional. *include* Include as another file from _path_. Expands shell syntax (see *wordexp*(3) for details). # PROFILE DIRECTIVES Profile directives are followed by space-separated arguments. Arguments can be quoted (with *"*) if they contain spaces. *output* An output directive adds an output to the profile. The criteria can either be an output name, an output description or "\*". The latter can be used to match any output. On *sway*(1), output names and descriptions can be obtained via *swaymsg -t get_outputs*. *exec* An exec directive executes a command when the profile was successfully applied. This can be used to update the compositor state to the profile when not done automatically. Commands are executed asynchronously and their order may not be preserved. If you need to execute sequential commands, you should collect in one exec statement or in a separate script. On *sway*(1) for example, *exec* can be used to move workspaces to the right output: ``` profile multihead { output eDP-1 enable output DP-1 enable transform 270 exec swaymsg workspace 1, move workspace to eDP-1 } ``` Note that some extra care must be taken with outputs identified by an output description as the real name may change: ``` profile complex { output "Some Other Company GTBZ 2525" mode 1920x1200 exec swaymsg workspace 1, move workspace to output '"Some Other Company GTBZ 2525"' } ``` # OUTPUT DIRECTIVES *enable*|*disable* Enables or disables the specified output. *mode* [--custom] x[@[Hz]] Configures the specified output to use the specified mode. Modes are a combination of width and height (in pixels) and a refresh rate (in Hz) that your display can be configured to use. Examples: ``` output HDMI-A-1 mode 1920x1080 output HDMI-A-1 mode 1920x1080@60Hz output HDMI-A-1 mode --custom 1280x720@60Hz ``` *position* , Places the output at the specified position in the global coordinates space. Example: ``` output HDMI-A-1 position 1600,0 ``` *scale* Scales the output by the specified scale factor. *transform* Sets the output transform. Can be one of "90", "180", "270" for a rotation; or "flipped", "flipped-90", "flipped-180", "flipped-270" for a rotation and a flip; or "normal" for no transform. *adaptive_sync* on|off Enables or disables adaptive synchronization (aka. Variable Refresh Rate). # AUTHORS Maintained by Simon Ser , who is assisted by other open-source contributors. For more information about kanshi development, see . # SEE ALSO *kanshi*(1) kanshi-1.5.1/doc/kanshictl.1.scd000066400000000000000000000007541455675313600163770ustar00rootroot00000000000000kanshictl(1) # NAME kanshictl - control the kanshi daemon remotely # SYNOPSIS *kanshictl* [options...] [command] # OPTIONS *-h, --help* Show help message and quit. # COMMANDS *reload* Reload the config file. *switch* Switch to a different profile. # AUTHORS Maintained by Simon Ser , who is assisted by other open-source contributors. For more information about kanshi development, see https://git.sr.ht/~emersion/kanshi. # SEE ALSO *kanshi*(1) kanshi-1.5.1/doc/meson.build000066400000000000000000000012511455675313600157200ustar00rootroot00000000000000scdoc = dependency( 'scdoc', version: '>=1.9.2', native: true, required: get_option('man-pages'), ) if not scdoc.found() subdir_done() endif scdoc_path = scdoc.get_variable('scdoc') mandir = get_option('mandir') man_files = [ 'kanshi.1.scd', 'kanshi.5.scd', ] if varlink.found() man_files += 'kanshictl.1.scd' endif foreach filename : man_files topic = filename.split('.')[-3].split('/')[-1] section = filename.split('.')[-2] output = '@0@.@1@'.format(topic, section) custom_target( output, input: filename, output: output, command: scdoc_path, capture: true, feed: true, install: true, install_dir: '@0@/man@1@'.format(mandir, section) ) endforeach kanshi-1.5.1/event-loop.c000066400000000000000000000072151455675313600152530ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include "kanshi.h" #if KANSHI_HAS_VARLINK #include #endif static int set_pipe_flags(int fd) { int flags = fcntl(fd, F_GETFL); if (flags == -1) { perror("fnctl F_GETFL failed"); return -1; } flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { perror("fnctl F_SETFL failed"); return -1; } flags = fcntl(fd, F_GETFD); if (flags == -1) { perror("fnctl F_GETFD failed"); return -1; } flags |= O_CLOEXEC; if (fcntl(fd, F_SETFD, flags) == -1) { perror("fnctl F_SETFD failed"); return -1; } return 0; } static int signal_pipefds[2]; static void signal_handler(int signum) { if (write(signal_pipefds[1], &signum, sizeof(signum)) == -1) { abort(); } } enum readfds_type { FD_WAYLAND, FD_SIGNAL, #if KANSHI_HAS_VARLINK FD_VARLINK, #endif FD_COUNT, }; int kanshi_main_loop(struct kanshi_state *state) { if (pipe(signal_pipefds) == -1) { perror("read from signalfd failed"); return EXIT_FAILURE; } if (set_pipe_flags(signal_pipefds[0]) == -1) { return EXIT_FAILURE; } if (set_pipe_flags(signal_pipefds[1]) == -1) { return EXIT_FAILURE; } struct sigaction action; sigfillset(&action.sa_mask); action.sa_flags = 0; action.sa_handler = signal_handler; sigaction(SIGINT, &action, NULL); sigaction(SIGQUIT, &action, NULL); sigaction(SIGTERM, &action, NULL); sigaction(SIGHUP, &action, NULL); struct pollfd readfds[FD_COUNT] = {0}; readfds[FD_WAYLAND].fd = wl_display_get_fd(state->display); readfds[FD_WAYLAND].events = POLLIN; readfds[FD_SIGNAL].fd = signal_pipefds[0]; readfds[FD_SIGNAL].events = POLLIN; #if KANSHI_HAS_VARLINK readfds[FD_VARLINK].fd = varlink_service_get_fd(state->service); readfds[FD_VARLINK].events = POLLIN; #endif while (state->running) { while (wl_display_prepare_read(state->display) != 0) { if (wl_display_dispatch_pending(state->display) == -1) { return EXIT_FAILURE; } } int ret; while (true) { ret = wl_display_flush(state->display); if (ret != -1 || errno != EAGAIN) { break; } } if (ret < 0 && errno != EPIPE) { goto read_error; } do { ret = poll(readfds, sizeof(readfds) / sizeof(readfds[0]), -1); } while (ret == -1 && errno == EINTR); /* will only be -1 if errno wasn't EINTR */ if (ret == -1) { goto read_error; } if (wl_display_read_events(state->display) == -1) { return EXIT_FAILURE; } #if KANSHI_HAS_VARLINK if (readfds[FD_VARLINK].revents & POLLIN) { long result = varlink_service_process_events(state->service); if (result != 0) { fprintf(stderr, "varlink_service_process_events failed: %s\n", varlink_error_string(-result)); return EXIT_FAILURE; } } #endif if (readfds[FD_SIGNAL].revents & POLLIN) { for (;;) { int signum; ssize_t s = read(readfds[FD_SIGNAL].fd, &signum, sizeof(signum)); if (s == 0) { break; } if (s < 0) { if (errno == EAGAIN) { break; } perror("read from signal pipe failed"); return EXIT_FAILURE; } if (s < (ssize_t) sizeof(signum)) { fprintf(stderr, "read too few bytes from signal pipe\n"); return EXIT_FAILURE; } switch (signum) { case SIGHUP: kanshi_reload_config(state, NULL, NULL); break; default: /* exiting after signal considered successful */ return EXIT_SUCCESS; } } } if (wl_display_dispatch_pending(state->display) == -1) { return EXIT_FAILURE; } } return EXIT_SUCCESS; read_error: wl_display_cancel_read(state->display); return EXIT_FAILURE; } kanshi-1.5.1/include/000077500000000000000000000000001455675313600144355ustar00rootroot00000000000000kanshi-1.5.1/include/config.h000066400000000000000000000016731455675313600160620ustar00rootroot00000000000000#ifndef KANSHI_CONFIG_H #define KANSHI_CONFIG_H #include #include enum kanshi_output_field { KANSHI_OUTPUT_ENABLED = 1 << 0, KANSHI_OUTPUT_MODE = 1 << 1, KANSHI_OUTPUT_POSITION = 1 << 2, KANSHI_OUTPUT_SCALE = 1 << 3, KANSHI_OUTPUT_TRANSFORM = 1 << 4, KANSHI_OUTPUT_ADAPTIVE_SYNC = 1 << 5, }; struct kanshi_profile_output { char *name; unsigned int fields; // enum kanshi_output_field struct wl_list link; bool enabled; struct { int width, height; int refresh; // mHz bool custom; } mode; struct { int x, y; } position; float scale; enum wl_output_transform transform; bool adaptive_sync; }; struct kanshi_profile_command { struct wl_list link; char *command; }; struct kanshi_profile { struct wl_list link; char *name; // Wildcard outputs are stored at the end of the list struct wl_list outputs; struct wl_list commands; }; struct kanshi_config { struct wl_list profiles; }; #endif kanshi-1.5.1/include/ipc.h000066400000000000000000000004021455675313600153550ustar00rootroot00000000000000#ifndef KANSHI_IPC_H #define KANSHI_IPC_H #include #include "kanshi.h" int kanshi_init_ipc(struct kanshi_state *state, int listen_fd); void kanshi_free_ipc(struct kanshi_state *state); int get_ipc_address(char *address, size_t size); #endif kanshi-1.5.1/include/kanshi.h000066400000000000000000000032251455675313600160650ustar00rootroot00000000000000#ifndef KANSHI_KANSHI_H #define KANSHI_KANSHI_H #include #include struct zwlr_output_manager_v1; struct kanshi_state; struct kanshi_head; struct kanshi_mode { struct kanshi_head *head; struct zwlr_output_mode_v1 *wlr_mode; struct wl_list link; int32_t width, height; int32_t refresh; // mHz bool preferred; }; struct kanshi_head { struct kanshi_state *state; struct zwlr_output_head_v1 *wlr_head; struct wl_list link; char *name, *description; char *make, *model, *serial_number; int32_t phys_width, phys_height; // mm struct wl_list modes; bool enabled; struct kanshi_mode *mode; struct { int32_t width, height; int32_t refresh; } custom_mode; int32_t x, y; enum wl_output_transform transform; double scale; bool adaptive_sync; }; struct kanshi_state { bool running; struct wl_display *display; struct zwlr_output_manager_v1 *output_manager; #if KANSHI_HAS_VARLINK struct VarlinkService *service; #endif struct kanshi_config *config; const char *config_arg; struct wl_list heads; uint32_t serial; struct kanshi_profile *current_profile; struct kanshi_profile *pending_profile; }; typedef void (*kanshi_apply_done_func)(void *data, bool success); struct kanshi_pending_profile { uint32_t serial; struct kanshi_state *state; struct kanshi_profile *profile; kanshi_apply_done_func callback; void *callback_data; }; bool kanshi_reload_config(struct kanshi_state *state, kanshi_apply_done_func callback, void *data); bool kanshi_switch(struct kanshi_state *state, struct kanshi_profile *profile, kanshi_apply_done_func callback, void *data); int kanshi_main_loop(struct kanshi_state *state); #endif kanshi-1.5.1/include/parser.h000066400000000000000000000006361455675313600161070ustar00rootroot00000000000000#ifndef KANSHI_PARSER_H #define KANSHI_PARSER_H #include struct kanshi_config; enum kanshi_token_type { KANSHI_TOKEN_LBRACKET, KANSHI_TOKEN_RBRACKET, KANSHI_TOKEN_STR, KANSHI_TOKEN_NEWLINE, }; struct kanshi_parser { FILE *f; int next; int line, col; enum kanshi_token_type tok_type; char tok_str[1024]; size_t tok_str_len; }; struct kanshi_config *parse_config(const char *path); #endif kanshi-1.5.1/ipc-addr.c000066400000000000000000000011201455675313600146330ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include "ipc.h" int get_ipc_address(char *address, size_t size) { const char *wayland_display = getenv("WAYLAND_DISPLAY"); const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); if (!wayland_display || !wayland_display[0]) { fprintf(stderr, "WAYLAND_DISPLAY is not set\n"); return -1; } if (!xdg_runtime_dir || !xdg_runtime_dir[0]) { fprintf(stderr, "XDG_RUNTIME_DIR is not set\n"); return -1; } return snprintf(address, size, "unix:%s/fr.emersion.kanshi.%s", xdg_runtime_dir, wayland_display); } kanshi-1.5.1/ipc.c000066400000000000000000000063351455675313600137400ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include "config.h" #include "kanshi.h" #include "ipc.h" static long reply_error(VarlinkCall *call, const char *name) { VarlinkObject *params = NULL; long ret = varlink_object_new(¶ms); if (ret < 0) { return ret; } ret = varlink_call_reply_error(call, name, params); varlink_object_unref(params); return ret; } static void apply_profile_done(void *data, bool success) { VarlinkCall *call = data; if (success) { varlink_call_reply(call, NULL, 0); } else { reply_error(call, "fr.emersion.kanshi.ProfileNotApplied"); } } static long handle_reload(VarlinkService *service, VarlinkCall *call, VarlinkObject *parameters, uint64_t flags, void *userdata) { struct kanshi_state *state = userdata; if (!kanshi_reload_config(state, apply_profile_done, call)) { return reply_error(call, "fr.emersion.kanshi.ProfileNotMatched"); } return 0; } static long handle_switch(VarlinkService *service, VarlinkCall *call, VarlinkObject *parameters, uint64_t flags, void *userdata) { struct kanshi_state *state = userdata; const char *profile_name; if (varlink_object_get_string(parameters, "profile", &profile_name) < 0) { return varlink_call_reply_invalid_parameter(call, "profile"); } struct kanshi_profile *profile; bool found = false; wl_list_for_each(profile, &state->config->profiles, link) { if (strcmp(profile->name, profile_name) == 0) { found = true; break; } } if (!found) { return reply_error(call, "fr.emersion.kanshi.ProfileNotFound"); } if (!kanshi_switch(state, profile, apply_profile_done, call)) { return reply_error(call, "fr.emersion.kanshi.ProfileNotMatched"); } return 0; } static int set_cloexec(int fd) { int flags = fcntl(fd, F_GETFD); if (flags < 0) { perror("fnctl(F_GETFD) failed"); return -1; } if (fcntl(fd, F_SETFD, flags | O_CLOEXEC) < 0) { perror("fnctl(F_SETFD) failed"); return -1; } return 0; } int kanshi_init_ipc(struct kanshi_state *state, int listen_fd) { if (listen_fd >= 0 && set_cloexec(listen_fd) < 0) { return -1; } VarlinkService *service; char address[PATH_MAX]; if (get_ipc_address(address, sizeof(address)) < 0) { return -1; } if (varlink_service_new(&service, "emersion", "kanshi", KANSHI_VERSION, "https://wayland.emersion.fr/kanshi/", address, listen_fd) < 0) { fprintf(stderr, "Couldn't start kanshi varlink service at %s.\n" "Is the kanshi daemon already running?\n", address); return -1; } const char *interface = "interface fr.emersion.kanshi\n" "method Reload() -> ()\n" "method Switch(profile: string) -> ()\n" "error ProfileNotFound()\n" "error ProfileNotMatched()\n" "error ProfileNotApplied()\n"; long result = varlink_service_add_interface(service, interface, "Reload", handle_reload, state, "Switch", handle_switch, state, NULL); if (result != 0) { fprintf(stderr, "varlink_service_add_interface failed: %s\n", varlink_error_string(-result)); varlink_service_free(service); return -1; } state->service = service; return 0; } void kanshi_free_ipc(struct kanshi_state *state) { if (state->service) { varlink_service_free(state->service); state->service = NULL; } } kanshi-1.5.1/main.c000066400000000000000000000532071455675313600141110ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "kanshi.h" #include "parser.h" #include "ipc.h" #include "wlr-output-management-unstable-v1-client-protocol.h" #define HEADS_MAX 64 static bool match_and_apply(struct kanshi_state *state, kanshi_apply_done_func callback, void *data); static bool match_profile_output(struct kanshi_profile_output *output, struct kanshi_head *head) { const char *make = head->make ? head->make : "Unknown"; const char *model = head->model ? head->model : "Unknown"; const char *serial_number = head->serial_number ? head->serial_number : "Unknown"; char identifier[1024]; assert(sizeof(identifier) >= strlen(make) + strlen(model) + strlen(serial_number) + 3); snprintf(identifier, sizeof(identifier), "%s %s %s", make, model, serial_number); return strcmp(output->name, "*") == 0 || strcmp(output->name, head->name) == 0 || strcmp(output->name, identifier) == 0; } static bool match_profile(struct kanshi_state *state, struct kanshi_profile *profile, struct kanshi_profile_output *matches[static HEADS_MAX]) { if (wl_list_length(&profile->outputs) != wl_list_length(&state->heads)) { return false; } memset(matches, 0, HEADS_MAX * sizeof(struct kanshi_head *)); // Wildcards are stored at the end of the list, so those will be matched // last struct kanshi_profile_output *profile_output; wl_list_for_each(profile_output, &profile->outputs, link) { bool output_matched = false; ssize_t i = -1; struct kanshi_head *head; wl_list_for_each(head, &state->heads, link) { i++; if (matches[i] != NULL) { continue; // already matched } if (match_profile_output(profile_output, head)) { matches[i] = profile_output; output_matched = true; break; } } if (!output_matched) { return false; } } return true; } static struct kanshi_profile *match(struct kanshi_state *state, struct kanshi_profile_output *matches[static HEADS_MAX]) { struct kanshi_profile *profile; wl_list_for_each(profile, &state->config->profiles, link) { if (match_profile(state, profile, matches)) { return profile; } } return NULL; } static void exec_command(char *cmd) { pid_t child, grandchild; // Fork process if ((child = fork()) == 0) { // Fork child process again so we can unparent the process setsid(); sigset_t set; sigemptyset(&set); sigprocmask(SIG_SETMASK, &set, NULL); struct sigaction action; sigfillset(&action.sa_mask); action.sa_flags = 0; action.sa_handler = SIG_DFL; sigaction(SIGINT, &action, NULL); sigaction(SIGQUIT, &action, NULL); sigaction(SIGTERM, &action, NULL); sigaction(SIGHUP, &action, NULL); if ((grandchild = fork()) == 0) { execl("/bin/sh", "/bin/sh", "-c", cmd, (void *)NULL); fprintf(stderr, "Executing command '%s' failed: %s\n", cmd, strerror(errno)); _exit(-1); } if (grandchild < 0) { fprintf(stderr, "Impossible to fork a new process to execute" " command '%s': %s\n", cmd, strerror(errno)); _exit(1); } _exit(0); // Close child process } if (child < 0) { perror("Impossible to fork a new process"); return; } // cleanup child process if (waitpid(child, NULL, 0) < 0) { perror("Impossible to clean up child process"); } } static void config_handle_succeeded(void *data, struct zwlr_output_configuration_v1 *config) { struct kanshi_pending_profile *pending = data; zwlr_output_configuration_v1_destroy(config); struct kanshi_state *state = pending->state; struct kanshi_profile *profile = pending->profile; struct kanshi_profile_command *command; wl_list_for_each(command, &profile->commands, link) { fprintf(stderr, "running command '%s'\n", command->command); exec_command(command->command); } fprintf(stderr, "configuration for profile '%s' applied\n", profile->name); state->current_profile = profile; if (profile == state->pending_profile) { state->pending_profile = NULL; } if (pending->callback != NULL) { pending->callback(pending->callback_data, true); } free(pending); } static void config_handle_failed(void *data, struct zwlr_output_configuration_v1 *config) { struct kanshi_pending_profile *pending = data; zwlr_output_configuration_v1_destroy(config); fprintf(stderr, "failed to apply configuration for profile '%s'\n", pending->profile->name); if (pending->profile == pending->state->pending_profile) { pending->state->pending_profile = NULL; } if (pending->callback != NULL) { pending->callback(pending->callback_data, false); } free(pending); } static void config_handle_cancelled(void *data, struct zwlr_output_configuration_v1 *config) { struct kanshi_pending_profile *pending = data; zwlr_output_configuration_v1_destroy(config); // Wait for new serial fprintf(stderr, "configuration for profile '%s' cancelled, retrying\n", pending->profile->name); if (pending->profile == pending->state->pending_profile) { pending->state->pending_profile = NULL; } if (pending->serial != pending->state->serial) { // We've already received a new serial, try re-applying the profile // immediately match_and_apply(pending->state, NULL, NULL); } if (pending->callback != NULL) { pending->callback(pending->callback_data, false); } free(pending); } static const struct zwlr_output_configuration_v1_listener config_listener = { .succeeded = config_handle_succeeded, .failed = config_handle_failed, .cancelled = config_handle_cancelled, }; static bool match_refresh(const struct kanshi_mode *mode, int refresh, int *delta) { int v = refresh - mode->refresh; int mode_delta = abs(v); /* If we have a refresh, pick one with the lowest delta from our target. * Doing a simple fuzzy match that picks the greatest (due to ordering) here can lead us to picking a refresh * such as 120.01 or 60.01, which is problematic for two reasons: * - Modes such as 4K 120.01Hz is too much for link bandwidth of DP 1.4 without DSC. * - It becomes out of phase with the majority of content being displayed. */ if (mode_delta < 50 && mode_delta < *delta) { *delta = mode_delta; return true; } return false; } static struct kanshi_mode *match_mode(struct kanshi_head *head, int width, int height, int refresh) { struct kanshi_mode *mode; struct kanshi_mode *last_match = NULL; int mode_delta = INT32_MAX; wl_list_for_each(mode, &head->modes, link) { if (mode->width != width || mode->height != height) { continue; } if (refresh) { if (match_refresh(mode, refresh, &mode_delta)) { last_match = mode; } } else { if (!last_match || mode->refresh > last_match->refresh) { last_match = mode; } } } return last_match; } static bool apply_profile(struct kanshi_state *state, struct kanshi_profile *profile, struct kanshi_profile_output **matches, kanshi_apply_done_func callback, void *data) { if (state->pending_profile == profile || state->current_profile == profile) { if (callback != NULL) { callback(data, true); } return true; } fprintf(stderr, "applying profile '%s'\n", profile->name); struct kanshi_pending_profile *pending = calloc(1, sizeof(*pending)); pending->serial = state->serial; pending->state = state; pending->profile = profile; pending->callback = callback; pending->callback_data = data; state->pending_profile = profile; struct zwlr_output_configuration_v1 *config = zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial); zwlr_output_configuration_v1_add_listener(config, &config_listener, pending); ssize_t i = -1; struct kanshi_head *head; wl_list_for_each(head, &state->heads, link) { i++; struct kanshi_profile_output *profile_output = matches[i]; fprintf(stderr, "applying profile output '%s' on connected head '%s'\n", profile_output->name, head->name); bool enabled = head->enabled; if (profile_output->fields & KANSHI_OUTPUT_ENABLED) { enabled = profile_output->enabled; } if (!enabled) { zwlr_output_configuration_v1_disable_head(config, head->wlr_head); continue; } struct zwlr_output_configuration_head_v1 *config_head = zwlr_output_configuration_v1_enable_head(config, head->wlr_head); if (profile_output->fields & KANSHI_OUTPUT_MODE) { if (profile_output->mode.custom) { fprintf(stderr, "applying the custom mode %s\n", profile_output->name); zwlr_output_configuration_head_v1_set_custom_mode(config_head, profile_output->mode.width, profile_output->mode.height, profile_output->mode.refresh); } else { struct kanshi_mode *mode = match_mode(head, profile_output->mode.width, profile_output->mode.height, profile_output->mode.refresh); if (mode == NULL) { fprintf(stderr, "output '%s' doesn't support mode '%dx%d@%fHz'\n", head->name, profile_output->mode.width, profile_output->mode.height, (float)profile_output->mode.refresh / 1000); goto error; } zwlr_output_configuration_head_v1_set_mode(config_head, mode->wlr_mode); } } if (profile_output->fields & KANSHI_OUTPUT_POSITION) { zwlr_output_configuration_head_v1_set_position(config_head, profile_output->position.x, profile_output->position.y); } if (profile_output->fields & KANSHI_OUTPUT_SCALE) { zwlr_output_configuration_head_v1_set_scale(config_head, wl_fixed_from_double(profile_output->scale)); } if (profile_output->fields & KANSHI_OUTPUT_TRANSFORM) { zwlr_output_configuration_head_v1_set_transform(config_head, profile_output->transform); } if (profile_output->fields & KANSHI_OUTPUT_ADAPTIVE_SYNC) { zwlr_output_configuration_head_v1_set_adaptive_sync(config_head, profile_output->adaptive_sync); } } zwlr_output_configuration_v1_apply(config); return true; error: free(pending); zwlr_output_configuration_v1_destroy(config); return false; } static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode, int32_t width, int32_t height) { struct kanshi_mode *mode = data; mode->width = width; mode->height = height; } static void mode_handle_refresh(void *data, struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) { struct kanshi_mode *mode = data; mode->refresh = refresh; } static void mode_handle_preferred(void *data, struct zwlr_output_mode_v1 *wlr_mode) { struct kanshi_mode *mode = data; mode->preferred = true; } static void mode_handle_finished(void *data, struct zwlr_output_mode_v1 *wlr_mode) { struct kanshi_mode *mode = data; wl_list_remove(&mode->link); if (zwlr_output_mode_v1_get_version(mode->wlr_mode) >= 3) { zwlr_output_mode_v1_release(mode->wlr_mode); } else { zwlr_output_mode_v1_destroy(mode->wlr_mode); } free(mode); } static const struct zwlr_output_mode_v1_listener mode_listener = { .size = mode_handle_size, .refresh = mode_handle_refresh, .preferred = mode_handle_preferred, .finished = mode_handle_finished, }; static void head_handle_name(void *data, struct zwlr_output_head_v1 *wlr_head, const char *name) { struct kanshi_head *head = data; head->name = strdup(name); } static void head_handle_description(void *data, struct zwlr_output_head_v1 *wlr_head, const char *description) { struct kanshi_head *head = data; head->description = strdup(description); } static void head_handle_physical_size(void *data, struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) { struct kanshi_head *head = data; head->phys_width = width; head->phys_height = height; } static void head_handle_mode(void *data, struct zwlr_output_head_v1 *wlr_head, struct zwlr_output_mode_v1 *wlr_mode) { struct kanshi_head *head = data; struct kanshi_mode *mode = calloc(1, sizeof(*mode)); mode->head = head; mode->wlr_mode = wlr_mode; wl_list_insert(head->modes.prev, &mode->link); zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode); } static void head_handle_enabled(void *data, struct zwlr_output_head_v1 *wlr_head, int32_t enabled) { struct kanshi_head *head = data; head->enabled = !!enabled; if (!enabled) { head->mode = NULL; } } static void head_handle_current_mode(void *data, struct zwlr_output_head_v1 *wlr_head, struct zwlr_output_mode_v1 *wlr_mode) { struct kanshi_head *head = data; struct kanshi_mode *mode; wl_list_for_each(mode, &head->modes, link) { if (mode->wlr_mode == wlr_mode) { head->mode = mode; return; } } fprintf(stderr, "received unknown current_mode\n"); head->mode = NULL; } static void head_handle_position(void *data, struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) { struct kanshi_head *head = data; head->x = x; head->y = y; } static void head_handle_transform(void *data, struct zwlr_output_head_v1 *wlr_head, int32_t transform) { struct kanshi_head *head = data; head->transform = transform; } static void head_handle_scale(void *data, struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) { struct kanshi_head *head = data; head->scale = wl_fixed_to_double(scale); } static void head_handle_finished(void *data, struct zwlr_output_head_v1 *wlr_head) { struct kanshi_head *head = data; wl_list_remove(&head->link); if (zwlr_output_head_v1_get_version(head->wlr_head) >= 3) { zwlr_output_head_v1_release(head->wlr_head); } else { zwlr_output_head_v1_destroy(head->wlr_head); } free(head->name); free(head->description); free(head->make); free(head->model); free(head->serial_number); free(head); } void head_handle_make(void *data, struct zwlr_output_head_v1 *zwlr_output_head_v1, const char *make) { struct kanshi_head *head = data; head->make = strdup(make); } void head_handle_model(void *data, struct zwlr_output_head_v1 *zwlr_output_head_v1, const char *model) { struct kanshi_head *head = data; head->model = strdup(model); } void head_handle_serial_number(void *data, struct zwlr_output_head_v1 *zwlr_output_head_v1, const char *serial_number) { struct kanshi_head *head = data; head->serial_number = strdup(serial_number); } static void head_handle_adaptive_sync(void *data, struct zwlr_output_head_v1 *zwlr_output_head_v1, uint32_t state) { struct kanshi_head *head = data; head->adaptive_sync = state; } static const struct zwlr_output_head_v1_listener head_listener = { .name = head_handle_name, .description = head_handle_description, .physical_size = head_handle_physical_size, .mode = head_handle_mode, .enabled = head_handle_enabled, .current_mode = head_handle_current_mode, .position = head_handle_position, .transform = head_handle_transform, .scale = head_handle_scale, .finished = head_handle_finished, .make = head_handle_make, .model = head_handle_model, .serial_number = head_handle_serial_number, .adaptive_sync = head_handle_adaptive_sync, }; static void output_manager_handle_head(void *data, struct zwlr_output_manager_v1 *manager, struct zwlr_output_head_v1 *wlr_head) { struct kanshi_state *state = data; struct kanshi_head *head = calloc(1, sizeof(*head)); head->state = state; head->wlr_head = wlr_head; head->scale = 1.0; wl_list_init(&head->modes); wl_list_insert(&state->heads, &head->link); zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head); } static bool match_and_apply(struct kanshi_state *state, kanshi_apply_done_func callback, void *data) { assert(wl_list_length(&state->heads) <= HEADS_MAX); // matches[i] gives the kanshi_profile_output for the i-th head struct kanshi_profile_output *matches[HEADS_MAX]; if (state->current_profile != NULL && match_profile(state, state->current_profile, matches)) { // keep the current profile if it still matches if (callback != NULL) { callback(data, true); } return true; } struct kanshi_profile *profile = match(state, matches); if (profile != NULL) { if (apply_profile(state, profile, matches, callback, data)) { return true; } } else { fprintf(stderr, "no profile matched\n"); } // If a profile failed to match or apply, forget the current profile. // This is necessary to make the following scenario work as expected: // have a profile for DP-1 and DP-2, disconnect DP-2, reconnect DP-2. // No profile will be matched in the intermediary state where only DP-1 is // connected, however the profile needs to be re-applied for the final // state. state->current_profile = NULL; return false; } bool kanshi_switch(struct kanshi_state *state, struct kanshi_profile *profile, kanshi_apply_done_func callback, void *data) { struct kanshi_profile_output *matches[HEADS_MAX]; if (!match_profile(state, profile, matches)) { return false; } return apply_profile(state, profile, matches, callback, data); } static void output_manager_handle_done(void *data, struct zwlr_output_manager_v1 *manager, uint32_t serial) { struct kanshi_state *state = data; state->serial = serial; match_and_apply(state, NULL, NULL); } static void output_manager_handle_finished(void *data, struct zwlr_output_manager_v1 *manager) { // This space is intentionally left blank } static const struct zwlr_output_manager_v1_listener output_manager_listener = { .head = output_manager_handle_head, .done = output_manager_handle_done, .finished = output_manager_handle_finished, }; static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { struct kanshi_state *state = data; if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) { uint32_t bind_version = 2; if (version >= 4) { bind_version = 4; } else if (version > bind_version) { bind_version = version; } state->output_manager = wl_registry_bind(registry, name, &zwlr_output_manager_v1_interface, bind_version); zwlr_output_manager_v1_add_listener(state->output_manager, &output_manager_listener, state); } } static void registry_handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { // This space is intentionally left blank } static const struct wl_registry_listener registry_listener = { .global = registry_handle_global, .global_remove = registry_handle_global_remove, }; static struct kanshi_config *read_config(const char *config) { if (config != NULL) { return parse_config(config); } const char config_filename[] = "kanshi/config"; char config_path[PATH_MAX]; const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); if (xdg_config_home != NULL) { snprintf(config_path, sizeof(config_path), "%s/%s", xdg_config_home, config_filename); } else if (home != NULL) { snprintf(config_path, sizeof(config_path), "%s/.config/%s", home, config_filename); } else { fprintf(stderr, "HOME not set\n"); return NULL; } return parse_config(config_path); } static void destroy_config(struct kanshi_config *config) { struct kanshi_profile *profile, *tmp_profile; wl_list_for_each_safe(profile, tmp_profile, &config->profiles, link) { struct kanshi_profile_output *output, *tmp_output; wl_list_for_each_safe(output, tmp_output, &profile->outputs, link) { free(output->name); wl_list_remove(&output->link); free(output); } struct kanshi_profile_command *command, *tmp_command; wl_list_for_each_safe(command, tmp_command, &profile->commands, link) { free(command->command); wl_list_remove(&command->link); free(command); } wl_list_remove(&profile->link); free(profile); } free(config); } bool kanshi_reload_config(struct kanshi_state *state, kanshi_apply_done_func callback, void *data) { fprintf(stderr, "reloading config\n"); struct kanshi_config *config = read_config(state->config_arg); if (config == NULL) { return false; } destroy_config(state->config); state->config = config; state->pending_profile = NULL; state->current_profile = NULL; return match_and_apply(state, callback, data); } static const char usage[] = "Usage: %s [options...]\n" " -h, --help Show help message and quit\n" " -c, --config Path to config file.\n"; static const struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"config", required_argument, 0, 'c'}, {"listen-fd", required_argument, 0, 'l'}, {0}, }; int main(int argc, char *argv[]) { const char *config_arg = NULL; #if KANSHI_HAS_VARLINK int listen_fd = -1; #endif int opt; while ((opt = getopt_long(argc, argv, "hc:l:", long_options, NULL)) != -1) { switch (opt) { case 'c': config_arg = optarg; break; case 'l': #if KANSHI_HAS_VARLINK listen_fd = strtol(optarg, NULL, 10); #else fprintf(stderr, "IPC support is disabled, " "-l/--listen-fd is not supported\n"); return EXIT_FAILURE; #endif break; case 'h': fprintf(stderr, usage, argv[0]); return EXIT_SUCCESS; default: fprintf(stderr, usage, argv[0]); return EXIT_FAILURE; } } struct kanshi_config *config = read_config(config_arg); if (config == NULL) { return EXIT_FAILURE; } struct wl_display *display = wl_display_connect(NULL); if (display == NULL) { fprintf(stderr, "failed to connect to display\n"); return EXIT_FAILURE; } struct kanshi_state state = { .running = true, .display = display, .config = config, .config_arg = config_arg, }; int ret = EXIT_SUCCESS; #if KANSHI_HAS_VARLINK if (kanshi_init_ipc(&state, listen_fd) != 0) { ret = EXIT_FAILURE; goto done; } #endif wl_list_init(&state.heads); struct wl_registry *registry = wl_display_get_registry(display); wl_registry_add_listener(registry, ®istry_listener, &state); if (wl_display_roundtrip(display) < 0) { fprintf(stderr, "wl_display_roundtrip() failed\n"); return EXIT_FAILURE; } if (state.output_manager == NULL) { fprintf(stderr, "compositor doesn't support " "wlr-output-management-unstable-v1\n"); ret = EXIT_FAILURE; goto done; } ret = kanshi_main_loop(&state); done: #if KANSHI_HAS_VARLINK kanshi_free_ipc(&state); #endif wl_display_disconnect(display); return ret; } kanshi-1.5.1/meson.build000066400000000000000000000030501455675313600151520ustar00rootroot00000000000000project( 'kanshi', 'c', version: '1.5.1', license: 'MIT', meson_version: '>=0.59.0', default_options: [ 'c_std=c11', 'warning_level=3', 'werror=true', ], ) cc = meson.get_compiler('c') add_project_arguments(cc.get_supported_arguments([ '-Wundef', '-Wlogical-op', '-Wmissing-include-dirs', '-Wold-style-definition', '-Wpointer-arith', '-Winit-self', '-Wfloat-equal', '-Wstrict-prototypes', '-Wredundant-decls', '-Wimplicit-fallthrough=2', '-Wendif-labels', '-Wstrict-aliasing=2', '-Woverflow', '-Wformat=2', '-Wno-missing-braces', '-Wno-missing-field-initializers', '-Wno-unused-parameter', ]), language: 'c') wayland_client = dependency('wayland-client') varlink = dependency('libvarlink', required: get_option('ipc')) add_project_arguments([ '-DKANSHI_VERSION="@0@"'.format(meson.project_version()), '-DKANSHI_HAS_VARLINK=@0@'.format(varlink.found().to_int()), ], language: 'c') subdir('protocol') kanshi_deps = [ wayland_client, ] kanshi_srcs = [ 'event-loop.c', 'main.c', 'parser.c', 'ipc-addr.c', ] if varlink.found() kanshi_deps += varlink kanshi_srcs += ['ipc.c', 'ipc-addr.c'] endif executable( meson.project_name(), kanshi_srcs + protocols_src, include_directories: 'include', dependencies: kanshi_deps, install: true, ) if varlink.found() executable( meson.project_name() + 'ctl', files( 'ctl.c', 'ipc-addr.c', ), include_directories: 'include', dependencies: [varlink], install: true, ) endif subdir('doc') summary({ 'Man pages': scdoc.found(), 'IPC': varlink.found(), }, bool_yn: true) kanshi-1.5.1/meson_options.txt000066400000000000000000000003041455675313600164440ustar00rootroot00000000000000option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('ipc', type: 'feature', value: 'auto', description: 'Enable remote control with varlink') kanshi-1.5.1/parser.c000066400000000000000000000352101455675313600144530ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include "config.h" #include "parser.h" static const char *token_type_str(enum kanshi_token_type t) { switch (t) { case KANSHI_TOKEN_LBRACKET: return "'{'"; case KANSHI_TOKEN_RBRACKET: return "'}'"; case KANSHI_TOKEN_STR: return "string"; case KANSHI_TOKEN_NEWLINE: return "newline"; } abort(); } static int parser_read_char(struct kanshi_parser *parser) { if (parser->next >= 0) { int ch = parser->next; parser->next = -1; return ch; } errno = 0; int ch = fgetc(parser->f); if (ch == EOF) { if (errno != 0) { fprintf(stderr, "fgetc failed: %s\n", strerror(errno)); } else { return '\0'; } return -1; } if (ch == '\n') { parser->line++; parser->col = 0; } else { parser->col++; } return ch; } static int parser_peek_char(struct kanshi_parser *parser) { int ch = parser_read_char(parser); parser->next = ch; return ch; } static bool parser_append_tok_ch(struct kanshi_parser *parser, char ch) { // Always keep enough room for a terminating NULL char if (parser->tok_str_len + 1 >= sizeof(parser->tok_str)) { fprintf(stderr, "string too long\n"); return false; } parser->tok_str[parser->tok_str_len] = ch; parser->tok_str_len++; return true; } static bool parser_read_quoted(struct kanshi_parser *parser, char quote_char) { while (1) { int ch = parser_read_char(parser); if (ch < 0) { return false; } else if (ch == '\0') { fprintf(stderr, "unterminated quoted string\n"); return false; } if (ch == quote_char) { parser->tok_str[parser->tok_str_len] = '\0'; return true; } if (!parser_append_tok_ch(parser, ch)) { return false; } } } static void parser_ignore_line(struct kanshi_parser *parser) { while (1) { int ch = parser_read_char(parser); if (ch < 0) { return; } if (ch == '\n' || ch == '\0') { return; } } } static bool parser_read_line(struct kanshi_parser *parser) { while (1) { int ch = parser_peek_char(parser); if (ch < 0) { return false; } if (ch == '\n' || ch == '\0') { parser->tok_str[parser->tok_str_len] = '\0'; return true; } if (!parser_append_tok_ch(parser, parser_read_char(parser))) { return false; } } } static bool parser_read_str(struct kanshi_parser *parser) { while (1) { int ch = parser_peek_char(parser); if (ch < 0) { return false; } if (isspace(ch) || ch == '{' || ch == '}' || ch == '\0') { parser->tok_str[parser->tok_str_len] = '\0'; return true; } if (!parser_append_tok_ch(parser, parser_read_char(parser))) { return false; } } } static bool parser_next_token(struct kanshi_parser *parser) { while (1) { int ch = parser_read_char(parser); if (ch < 0) { return false; } if (ch == '{') { parser->tok_type = KANSHI_TOKEN_LBRACKET; return true; } else if (ch == '}') { parser->tok_type = KANSHI_TOKEN_RBRACKET; return true; } else if (ch == '\n') { parser->tok_type = KANSHI_TOKEN_NEWLINE; return true; } else if (isspace(ch)) { continue; } else if (ch == '"' || ch == '\'') { parser->tok_type = KANSHI_TOKEN_STR; parser->tok_str_len = 0; return parser_read_quoted(parser, ch); } else if (ch == '#') { parser_ignore_line(parser); parser->tok_type = KANSHI_TOKEN_NEWLINE; return true; } else { parser->tok_type = KANSHI_TOKEN_STR; parser->tok_str[0] = ch; parser->tok_str_len = 1; return parser_read_str(parser); } } } static bool parser_expect_token(struct kanshi_parser *parser, enum kanshi_token_type want) { if (!parser_next_token(parser)) { return false; } if (parser->tok_type != want) { fprintf(stderr, "expected %s, got %s\n", token_type_str(want), token_type_str(parser->tok_type)); return false; } return true; } static bool parse_int(int *dst, const char *str) { char *end; errno = 0; int v = strtol(str, &end, 10); if (errno != 0 || end[0] != '\0' || str[0] == '\0') { return false; } *dst = v; return true; } static bool parse_mode(struct kanshi_profile_output *output, char *str) { const char *width = strtok(str, "x"); const char *height = strtok(NULL, "@"); const char *refresh = strtok(NULL, ""); if (width == NULL || height == NULL) { fprintf(stderr, "invalid output mode: missing width/height\n"); return false; } if (!parse_int(&output->mode.width, width)) { fprintf(stderr, "invalid output mode: invalid width\n"); return false; } if (!parse_int(&output->mode.height, height)) { fprintf(stderr, "invalid output mode: invalid height\n"); return false; } if (refresh != NULL) { char *end; errno = 0; float v = strtof(refresh, &end); if (errno != 0 || (end[0] != '\0' && strcmp(end, "Hz") != 0) || str[0] == '\0') { fprintf(stderr, "invalid output mode: invalid refresh rate\n"); return false; } output->mode.refresh = v * 1000; } return true; } static bool parse_position(struct kanshi_profile_output *output, char *str) { const char *x = strtok(str, ","); const char *y = strtok(NULL, ""); if (x == NULL || y == NULL) { fprintf(stderr, "invalid output position: missing x/y\n"); return false; } if (!parse_int(&output->position.x, x)) { fprintf(stderr, "invalid output position: invalid x\n"); return false; } if (!parse_int(&output->position.y, y)) { fprintf(stderr, "invalid output position: invalid y\n"); return false; } return true; } static bool parse_float(float *dst, const char *str) { char *end; errno = 0; float v = strtof(str, &end); if (errno != 0 || end[0] != '\0' || str[0] == '\0') { return false; } *dst = v; return true; } static bool parse_transform(enum wl_output_transform *dst, const char *str) { if (strcmp(str, "normal") == 0) { *dst = WL_OUTPUT_TRANSFORM_NORMAL; } else if (strcmp(str, "90") == 0) { *dst = WL_OUTPUT_TRANSFORM_90; } else if (strcmp(str, "180") == 0) { *dst = WL_OUTPUT_TRANSFORM_180; } else if (strcmp(str, "270") == 0) { *dst = WL_OUTPUT_TRANSFORM_270; } else if (strcmp(str, "flipped") == 0) { *dst = WL_OUTPUT_TRANSFORM_FLIPPED; } else if (strcmp(str, "flipped-90") == 0) { *dst = WL_OUTPUT_TRANSFORM_FLIPPED_90; } else if (strcmp(str, "flipped-180") == 0) { *dst = WL_OUTPUT_TRANSFORM_FLIPPED_180; } else if (strcmp(str, "flipped-270") == 0) { *dst = WL_OUTPUT_TRANSFORM_FLIPPED_270; } else { return false; } return true; } static bool parse_bool(bool *dst, const char *str) { if (strcmp(str, "on") == 0) { *dst = true; } else if (strcmp(str, "off") == 0) { *dst = false; } else { return false; } return true; } static struct kanshi_profile_output *parse_profile_output( struct kanshi_parser *parser) { struct kanshi_profile_output *output = calloc(1, sizeof(*output)); if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) { return NULL; } output->name = strdup(parser->tok_str); bool has_key = false; enum kanshi_output_field key = 0; while (1) { if (!parser_next_token(parser)) { return NULL; } switch (parser->tok_type) { case KANSHI_TOKEN_STR: if (has_key) { char *value = parser->tok_str; switch (key) { case KANSHI_OUTPUT_MODE: if (strcmp(value, "--custom") == 0) { output->mode.custom = true; continue; } if (!parse_mode(output, value)) { return NULL; } break; case KANSHI_OUTPUT_POSITION: if (!parse_position(output, value)) { return NULL; } break; case KANSHI_OUTPUT_SCALE: if (!parse_float(&output->scale, value)) { fprintf(stderr, "invalid output scale\n"); return NULL; } break; case KANSHI_OUTPUT_TRANSFORM: if (!parse_transform(&output->transform, value)) { fprintf(stderr, "invalid output transform\n"); return NULL; } break; case KANSHI_OUTPUT_ADAPTIVE_SYNC: if (!parse_bool(&output->adaptive_sync, value)) { fprintf(stderr, "invalid output adaptive_sync\n"); return NULL; } break; default: abort(); } has_key = false; output->fields |= key; } else { has_key = true; const char *key_str = parser->tok_str; if (strcmp(key_str, "enable") == 0) { output->enabled = true; output->fields |= KANSHI_OUTPUT_ENABLED; has_key = false; } else if (strcmp(key_str, "disable") == 0) { output->enabled = false; output->fields |= KANSHI_OUTPUT_ENABLED; has_key = false; } else if (strcmp(key_str, "mode") == 0) { key = KANSHI_OUTPUT_MODE; } else if (strcmp(key_str, "position") == 0) { key = KANSHI_OUTPUT_POSITION; } else if (strcmp(key_str, "scale") == 0) { key = KANSHI_OUTPUT_SCALE; } else if (strcmp(key_str, "transform") == 0) { key = KANSHI_OUTPUT_TRANSFORM; } else if (strcmp(key_str, "adaptive_sync") == 0) { key = KANSHI_OUTPUT_ADAPTIVE_SYNC; } else { fprintf(stderr, "unknown directive '%s' in profile output '%s'\n", key_str, output->name); return NULL; } } break; case KANSHI_TOKEN_NEWLINE: return output; default: fprintf(stderr, "unexpected %s in output\n", token_type_str(parser->tok_type)); return NULL; } } } static struct kanshi_profile_command *parse_profile_command( struct kanshi_parser *parser) { // Skip the 'exec' directive. if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) { return NULL; } if (!parser_read_line(parser)) { return NULL; } if (parser->tok_str_len <= 0) { fprintf(stderr, "Ignoring empty command in config file on line %d\n", parser->line); return NULL; } struct kanshi_profile_command *command = calloc(1, sizeof(*command)); command->command = strdup(parser->tok_str); return command; } static struct kanshi_profile *parse_profile(struct kanshi_parser *parser) { struct kanshi_profile *profile = calloc(1, sizeof(*profile)); wl_list_init(&profile->outputs); wl_list_init(&profile->commands); if (!parser_next_token(parser)) { return NULL; } switch (parser->tok_type) { case KANSHI_TOKEN_LBRACKET: break; case KANSHI_TOKEN_STR: // Parse an optional profile name profile->name = strdup(parser->tok_str); if (!parser_expect_token(parser, KANSHI_TOKEN_LBRACKET)) { return NULL; } break; default: fprintf(stderr, "unexpected %s, expected '{' or a profile name\n", token_type_str(parser->tok_type)); } // Use the bracket position to generate a default profile name if (profile->name == NULL) { char generated_name[100]; int ret = snprintf(generated_name, sizeof(generated_name), "", parser->line, parser->col); if (ret >= 0) { profile->name = strdup(generated_name); } else { profile->name = strdup(""); } } // Parse the profile commands until the closing bracket while (1) { if (!parser_next_token(parser)) { return NULL; } switch (parser->tok_type) { case KANSHI_TOKEN_RBRACKET: return profile; case KANSHI_TOKEN_STR:; const char *directive = parser->tok_str; if (strcmp(directive, "output") == 0) { struct kanshi_profile_output *output = parse_profile_output(parser); if (output == NULL) { return NULL; } // Store wildcard outputs at the end of the list if (strcmp(output->name, "*") == 0) { wl_list_insert(profile->outputs.prev, &output->link); } else { wl_list_insert(&profile->outputs, &output->link); } } else if (strcmp(directive, "exec") == 0) { struct kanshi_profile_command *command = parse_profile_command(parser); if (command == NULL) { return NULL; } // Insert commands at the end to preserve order wl_list_insert(profile->commands.prev, &command->link); } else { fprintf(stderr, "unknown directive '%s' in profile '%s'\n", directive, profile->name); return NULL; } break; case KANSHI_TOKEN_NEWLINE: break; // No-op default: fprintf(stderr, "unexpected %s in profile '%s'\n", token_type_str(parser->tok_type), profile->name); return NULL; } } } static bool parse_config_file(const char *path, struct kanshi_config *config); static bool parse_include_command(struct kanshi_parser *parser, struct kanshi_config *config) { // Skip the 'include' directive. if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) { return false; } if (!parser_read_line(parser)) { return false; } if (parser->tok_str_len <= 0) { return true; } wordexp_t p; if (wordexp(parser->tok_str, &p, WRDE_SHOWERR | WRDE_UNDEF) != 0) { fprintf(stderr, "Could not expand include path: '%s'\n", parser->tok_str); return false; } char **w = p.we_wordv; for (size_t idx = 0; idx < p.we_wordc; idx++) { if (!parse_config_file(w[idx], config)) { fprintf(stderr, "Could not parse included config: '%s'\n", w[idx]); wordfree(&p); return false; } } wordfree(&p); return true; } static bool _parse_config(struct kanshi_parser *parser, struct kanshi_config *config) { while (1) { int ch = parser_peek_char(parser); if (ch < 0) { return false; } else if (ch == 0) { return true; } else if (ch == '#') { parser_ignore_line(parser); continue; } else if (isspace(ch)) { parser_read_char(parser); continue; } if (ch == '{') { // Legacy profile syntax without a profile directive struct kanshi_profile *profile = parse_profile(parser); if (!profile) { return false; } wl_list_insert(config->profiles.prev, &profile->link); } else { if (!parser_expect_token(parser, KANSHI_TOKEN_STR)) { return false; } const char *directive = parser->tok_str; if (strcmp(parser->tok_str, "profile") == 0) { struct kanshi_profile *profile = parse_profile(parser); if (!profile) { return false; } wl_list_insert(config->profiles.prev, &profile->link); } else if (strcmp(parser->tok_str, "include") == 0) { if (!parse_include_command(parser, config)) { return false; } } else { fprintf(stderr, "unknown directive '%s'\n", directive); return false; } } } } static bool parse_config_file(const char *path, struct kanshi_config *config) { FILE *f = fopen(path, "r"); if (f == NULL) { fprintf(stderr, "failed to open file %s: %s\n", path, strerror(errno)); return false; } struct kanshi_parser parser = { .f = f, .next = -1, .line = 1, }; bool res = _parse_config(&parser, config); fclose(f); if (!res) { fprintf(stderr, "failed to parse config file: " "error on line %d, column %d\n", parser.line, parser.col); return false; } return true; } struct kanshi_config *parse_config(const char *path) { struct kanshi_config *config = calloc(1, sizeof(*config)); if (config == NULL) { return NULL; } wl_list_init(&config->profiles); if (!parse_config_file(path, config)) { free(config); return NULL; } return config; } kanshi-1.5.1/protocol/000077500000000000000000000000001455675313600146535ustar00rootroot00000000000000kanshi-1.5.1/protocol/meson.build000066400000000000000000000014551455675313600170220ustar00rootroot00000000000000wayland_scanner = find_program('wayland-scanner') wayland_scanner = dependency('wayland-scanner', version: '>=1.14.91', native: true) wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner') wayland_scanner_prog = find_program(wayland_scanner_path, native: true) wayland_scanner_code = generator( wayland_scanner_prog, output: '@BASENAME@-protocol.c', arguments: ['private-code', '@INPUT@', '@OUTPUT@'], ) wayland_scanner_client = generator( wayland_scanner_prog, output: '@BASENAME@-client-protocol.h', arguments: ['client-header', '@INPUT@', '@OUTPUT@'], ) protocols = [ 'wlr-output-management-unstable-v1.xml', ] protocols_src = [] foreach xml : protocols protocols_src += wayland_scanner_code.process(xml) protocols_src += wayland_scanner_client.process(xml) endforeach kanshi-1.5.1/protocol/wlr-output-management-unstable-v1.xml000066400000000000000000000622661455675313600240240ustar00rootroot00000000000000 Copyright © 2019 Purism SPC Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. This protocol exposes interfaces to obtain and modify output device configuration. Warning! The protocol described in this file is experimental and backward incompatible changes may be made. Backward compatible changes may be added together with the corresponding interface version bump. Backward incompatible changes are done by bumping the version number in the protocol and interface names and resetting the interface version. Once the protocol is to be declared stable, the 'z' prefix and the version number in the protocol and interface names are removed and the interface version number is reset. This interface is a manager that allows reading and writing the current output device configuration. Output devices that display pixels (e.g. a physical monitor or a virtual output in a window) are represented as heads. Heads cannot be created nor destroyed by the client, but they can be enabled or disabled and their properties can be changed. Each head may have one or more available modes. Whenever a head appears (e.g. a monitor is plugged in), it will be advertised via the head event. Immediately after the output manager is bound, all current heads are advertised. Whenever a head's properties change, the relevant wlr_output_head events will be sent. Not all head properties will be sent: only properties that have changed need to. Whenever a head disappears (e.g. a monitor is unplugged), a wlr_output_head.finished event will be sent. After one or more heads appear, change or disappear, the done event will be sent. It carries a serial which can be used in a create_configuration request to update heads properties. The information obtained from this protocol should only be used for output configuration purposes. This protocol is not designed to be a generic output property advertisement protocol for regular clients. Instead, protocols such as xdg-output should be used. This event introduces a new head. This happens whenever a new head appears (e.g. a monitor is plugged in) or after the output manager is bound. This event is sent after all information has been sent after binding to the output manager object and after any subsequent changes. This applies to child head and mode objects as well. In other words, this event is sent whenever a head or mode is created or destroyed and whenever one of their properties has been changed. Not all state is re-sent each time the current configuration changes: only the actual changes are sent. This allows changes to the output configuration to be seen as atomic, even if they happen via multiple events. A serial is sent to be used in a future create_configuration request. Create a new output configuration object. This allows to update head properties. Indicates the client no longer wishes to receive events for output configuration changes. However the compositor may emit further events, until the finished event is emitted. The client must not send any more requests after this one. This event indicates that the compositor is done sending manager events. The compositor will destroy the object immediately after sending this event, so it will become invalid and the client should release any resources associated with it. A head is an output device. The difference between a wl_output object and a head is that heads are advertised even if they are turned off. A head object only advertises properties and cannot be used directly to change them. A head has some read-only properties: modes, name, description and physical_size. These cannot be changed by clients. Other properties can be updated via a wlr_output_configuration object. Properties sent via this interface are applied atomically via the wlr_output_manager.done event. No guarantees are made regarding the order in which properties are sent. This event describes the head name. The naming convention is compositor defined, but limited to alphanumeric characters and dashes (-). Each name is unique among all wlr_output_head objects, but if a wlr_output_head object is destroyed the same name may be reused later. The names will also remain consistent across sessions with the same hardware and software configuration. Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do not assume that the name is a reflection of an underlying DRM connector, X11 connection, etc. If the compositor implements the xdg-output protocol and this head is enabled, the xdg_output.name event must report the same name. The name event is sent after a wlr_output_head object is created. This event is only sent once per object, and the name does not change over the lifetime of the wlr_output_head object. This event describes a human-readable description of the head. The description is a UTF-8 string with no convention defined for its contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 output via :1'. However, do not assume that the name is a reflection of the make, model, serial of the underlying DRM connector or the display name of the underlying X11 connection, etc. If the compositor implements xdg-output and this head is enabled, the xdg_output.description must report the same description. The description event is sent after a wlr_output_head object is created. This event is only sent once per object, and the description does not change over the lifetime of the wlr_output_head object. This event describes the physical size of the head. This event is only sent if the head has a physical size (e.g. is not a projector or a virtual device). This event introduces a mode for this head. It is sent once per supported mode. This event describes whether the head is enabled. A disabled head is not mapped to a region of the global compositor space. When a head is disabled, some properties (current_mode, position, transform and scale) are irrelevant. This event describes the mode currently in use for this head. It is only sent if the output is enabled. This events describes the position of the head in the global compositor space. It is only sent if the output is enabled. This event describes the transformation currently applied to the head. It is only sent if the output is enabled. This events describes the scale of the head in the global compositor space. It is only sent if the output is enabled. This event indicates that the head is no longer available. The head object becomes inert. Clients should send a destroy request and release any resources associated with it. This event describes the manufacturer of the head. This must report the same make as the wl_output interface does in its geometry event. Together with the model and serial_number events the purpose is to allow clients to recognize heads from previous sessions and for example load head-specific configurations back. It is not guaranteed this event will be ever sent. A reason for that can be that the compositor does not have information about the make of the head or the definition of a make is not sensible in the current setup, for example in a virtual session. Clients can still try to identify the head by available information from other events but should be aware that there is an increased risk of false positives. It is not recommended to display the make string in UI to users. For that the string provided by the description event should be preferred. This event describes the model of the head. This must report the same model as the wl_output interface does in its geometry event. Together with the make and serial_number events the purpose is to allow clients to recognize heads from previous sessions and for example load head-specific configurations back. It is not guaranteed this event will be ever sent. A reason for that can be that the compositor does not have information about the model of the head or the definition of a model is not sensible in the current setup, for example in a virtual session. Clients can still try to identify the head by available information from other events but should be aware that there is an increased risk of false positives. It is not recommended to display the model string in UI to users. For that the string provided by the description event should be preferred. This event describes the serial number of the head. Together with the make and model events the purpose is to allow clients to recognize heads from previous sessions and for example load head- specific configurations back. It is not guaranteed this event will be ever sent. A reason for that can be that the compositor does not have information about the serial number of the head or the definition of a serial number is not sensible in the current setup. Clients can still try to identify the head by available information from other events but should be aware that there is an increased risk of false positives. It is not recommended to display the serial_number string in UI to users. For that the string provided by the description event should be preferred. This request indicates that the client will no longer use this head object. This event describes whether adaptive sync is currently enabled for the head or not. Adaptive sync is also known as Variable Refresh Rate or VRR. This object describes an output mode. Some heads don't support output modes, in which case modes won't be advertised. Properties sent via this interface are applied atomically via the wlr_output_manager.done event. No guarantees are made regarding the order in which properties are sent. This event describes the mode size. The size is given in physical hardware units of the output device. This is not necessarily the same as the output size in the global compositor space. For instance, the output may be scaled or transformed. This event describes the mode's fixed vertical refresh rate. It is only sent if the mode has a fixed refresh rate. This event advertises this mode as preferred. This event indicates that the mode is no longer available. The mode object becomes inert. Clients should send a destroy request and release any resources associated with it. This request indicates that the client will no longer use this mode object. This object is used by the client to describe a full output configuration. First, the client needs to setup the output configuration. Each head can be either enabled (and configured) or disabled. It is a protocol error to send two enable_head or disable_head requests with the same head. It is a protocol error to omit a head in a configuration. Then, the client can apply or test the configuration. The compositor will then reply with a succeeded, failed or cancelled event. Finally the client should destroy the configuration object. Enable a head. This request creates a head configuration object that can be used to change the head's properties. Disable a head. Apply the new output configuration. In case the configuration is successfully applied, there is no guarantee that the new output state matches completely the requested configuration. For instance, a compositor might round the scale if it doesn't support fractional scaling. After this request has been sent, the compositor must respond with an succeeded, failed or cancelled event. Sending a request that isn't the destructor is a protocol error. Test the new output configuration. The configuration won't be applied, but will only be validated. Even if the compositor succeeds to test a configuration, applying it may fail. After this request has been sent, the compositor must respond with an succeeded, failed or cancelled event. Sending a request that isn't the destructor is a protocol error. Sent after the compositor has successfully applied the changes or tested them. Upon receiving this event, the client should destroy this object. If the current configuration has changed, events to describe the changes will be sent followed by a wlr_output_manager.done event. Sent if the compositor rejects the changes or failed to apply them. The compositor should revert any changes made by the apply request that triggered this event. Upon receiving this event, the client should destroy this object. Sent if the compositor cancels the configuration because the state of an output changed and the client has outdated information (e.g. after an output has been hotplugged). The client can create a new configuration with a newer serial and try again. Upon receiving this event, the client should destroy this object. Using this request a client can tell the compositor that it is not going to use the configuration object anymore. Any changes to the outputs that have not been applied will be discarded. This request also destroys wlr_output_configuration_head objects created via this object. This object is used by the client to update a single head's configuration. It is a protocol error to set the same property twice. This request sets the head's mode. This request assigns a custom mode to the head. The size is given in physical hardware units of the output device. If set to zero, the refresh rate is unspecified. It is a protocol error to set both a mode and a custom mode. This request sets the head's position in the global compositor space. This request sets the head's transform. This request sets the head's scale. This request enables/disables adaptive sync. Adaptive sync is also known as Variable Refresh Rate or VRR.