From e208957fa2038f72b1a64757f8b5d2b6d4d0cada Mon Sep 17 00:00:00 2001 From: Jason Francis Date: Sun, 8 Sep 2019 18:29:31 -0400 Subject: [PATCH] Add integration with kanshi daemon --- meson_options.txt | 2 + src/kanshi.h | 95 +++++++ src/meson.build | 29 +- src/outputs.c | 132 ++++++++- src/parser.c | 668 ++++++++++++++++++++++++++++++++++++++++++++++ src/wdisplays.h | 5 +- 6 files changed, 910 insertions(+), 21 deletions(-) create mode 100644 meson_options.txt create mode 100644 src/kanshi.h create mode 100644 src/parser.c diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..e597263 --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,2 @@ +option('kanshi', type: 'feature', value: 'auto', description: 'Enable integration with the kanshi daemon') + diff --git a/src/kanshi.h b/src/kanshi.h new file mode 100644 index 0000000..85b94ef --- /dev/null +++ b/src/kanshi.h @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2017-2019 emersion + * Copyright (C) 2019 cyclopsian + + * 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 X CONSORTIUM 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. + */ + +/* + * Parts of this file are taken from emersion/kanshi: + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/parser.h + */ + +#ifndef WDISPLAYS_KANSHI_H +#define WDISPLAYS_KANSHI_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, +}; + +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 + } mode; + struct { + int x, y; + } position; + float scale; + enum wl_output_transform transform; +}; + + +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 commands; + struct wl_list outputs; +}; + +struct kanshi_config { + struct wl_list profiles; +}; + +/* + * Loads the kanshi config from the given file. + */ +struct kanshi_config *kanshi_parse_config(const char *path); + +/* + * Saves the kanshi config to the given file. + */ +void kanshi_save_config(const char *path, struct kanshi_config *config); + +/* + * Destroys the config structure. + */ +void kanshi_destroy_config(struct kanshi_config *config); + +#endif diff --git a/src/meson.build b/src/meson.build index 26cbd2f..231f649 100644 --- a/src/meson.build +++ b/src/meson.build @@ -7,15 +7,31 @@ gtk = dependency('gtk+-3.0') assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') epoxy = dependency('epoxy') +wdisplays_src = files( + 'main.c', + 'outputs.c', + 'render.c', + 'glviewport.c', + 'overlay.c' +) +wdisplays_args = [] + +kanshictl = find_program('kanshictl', required: get_option('kanshi')) +if kanshictl.found() + wdisplays_src += files( + 'parser.c' + ) + wdisplays_args += [ + '-DHAVE_KANSHI', + '-DKANSHICTL_PATH="@0@"'.format(kanshictl.path()) + ] +endif + executable( 'wdisplays', [ - 'main.c', - 'outputs.c', - 'render.c', - 'glviewport.c', - 'overlay.c', - resources, + wdisplays_src, + resources ], dependencies : [ m_dep, @@ -25,5 +41,6 @@ executable( epoxy, gtk ], + c_args: wdisplays_args, install: true ) diff --git a/src/outputs.c b/src/outputs.c index 085733e..125617d 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -27,7 +27,6 @@ * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c */ -#define _GNU_SOURCE #include #include #include @@ -40,7 +39,12 @@ #include #include +#include + #include "wdisplays.h" +#ifdef HAVE_KANSHI +#include "kanshi.h" +#endif #include "wlr-output-management-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" @@ -66,6 +70,109 @@ static void destroy_pending(struct wd_pending_config *pending) { free(pending); } +#ifdef HAVE_KANSHI +static struct kanshi_profile *find_active_profile( + const struct kanshi_config *config, const struct wl_list *outputs) { + struct kanshi_profile *profile; + int head_count = wl_list_length(outputs); + wl_list_for_each(profile, &config->profiles, link) { + unsigned found_count = 0; + struct kanshi_profile_output *output; + wl_list_for_each(output, &profile->outputs, link) { + struct wd_head_config *head; + wl_list_for_each(head, outputs, link) { + if (strcmp(output->name, head->head->name) == 0) { + found_count++; + break; + } + } + } + if (found_count == head_count) { + return profile; + } + } + return NULL; +} + +static void update_profile_output(struct kanshi_profile_output *output, + const struct wd_head_config *head) { + output->enabled = head->enabled; + output->fields |= KANSHI_OUTPUT_ENABLED; + if (head->enabled) { + output->fields |= KANSHI_OUTPUT_MODE | KANSHI_OUTPUT_POSITION + | KANSHI_OUTPUT_SCALE | KANSHI_OUTPUT_TRANSFORM; + output->mode.width = head->width; + output->mode.height = head->height; + output->mode.refresh = head->refresh; + output->position.x = head->x; + output->position.y = head->y; + output->scale = head->scale; + output->transform = head->transform; + } +} + +static bool save_config(const struct wl_list *outputs) { + g_autofree const char *config_dir = g_strjoin("/", + g_get_user_config_dir(), "kanshi/config.d", NULL); + if (g_mkdir_with_parents(config_dir, 0700) == -1) { + fprintf(stderr, "g_mkdir_with_parents failed: %s: %s\n", + config_dir, strerror(errno)); + return false; + } + g_autofree const char *config_path = g_strjoin("/", + config_dir, "50-wdisplays", NULL); + struct kanshi_config *config = kanshi_parse_config(config_path); + if (config == NULL) { + config = calloc(1, sizeof(*config)); + wl_list_init(&config->profiles); + } + struct kanshi_profile *profile = find_active_profile(config, outputs); + if (profile == NULL) { + profile = calloc(1, sizeof(*profile)); + wl_list_init(&profile->outputs); + struct wd_head_config *head; + wl_list_for_each(head, outputs, link) { + struct kanshi_profile_output *output = calloc(1, sizeof(*output)); + output->name = strdup(head->head->name); + update_profile_output(output, head); + wl_list_insert(profile->outputs.prev, &output->link); + } + wl_list_insert(&config->profiles, &profile->link); + } else { + struct kanshi_profile_output *output; + wl_list_for_each(output, &profile->outputs, link) { + struct wd_head_config *head; + wl_list_for_each(head, outputs, link) { + if (strcmp(output->name, head->head->name) == 0) { + update_profile_output(output, head); + break; + } + } + } + } + + kanshi_save_config(config_path, config); + kanshi_destroy_config(config); + + GError *error = NULL; + gchar *argv[] = { KANSHICTL_PATH, "reload", NULL }; + gint status; + g_spawn_sync(NULL, argv, NULL, G_SPAWN_DEFAULT, NULL, NULL, + NULL, NULL, &status, &error); + if (error != NULL) { + fprintf(stderr, "Could not execute " KANSHICTL_PATH ": %s\n", + error->message); + g_error_free(error); + return false; + } + if (status != EXIT_SUCCESS) { + fprintf(stderr, "Could not execute " KANSHICTL_PATH "\n"); + return false; + } + return true; +} +#endif + static void config_handle_succeeded(void *data, struct zwlr_output_configuration_v1 *config) { struct wd_pending_config *pending = data; @@ -103,13 +210,21 @@ static const struct zwlr_output_configuration_v1_listener config_listener = { void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, struct wl_display *display) { - struct zwlr_output_configuration_v1 *config = - zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial); - struct wd_pending_config *pending = calloc(1, sizeof(*pending)); pending->state = state; pending->outputs = new_outputs; +#ifdef HAVE_KANSHI + if (save_config(new_outputs)) { + wd_ui_apply_done(pending->state, pending->outputs); + destroy_pending(pending); + return; + } +#endif + + 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; @@ -177,20 +292,13 @@ static void wd_frame_destroy(struct wd_frame *frame) { } static int create_shm_file(size_t size, const char *fmt, ...) { - char *shm_name = NULL; int fd = -1; va_list vl; va_start(vl, fmt); - int result = vasprintf(&shm_name, fmt, vl); + char *shm_name = g_strdup_vprintf(fmt, vl); va_end(vl); - if (result == -1) { - fprintf(stderr, "asprintf: %s\n", strerror(errno)); - shm_name = NULL; - return -1; - } - fd = shm_open(shm_name, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR); if (fd == -1) { fprintf(stderr, "shm_open: %s\n", strerror(errno)); diff --git a/src/parser.c b/src/parser.c new file mode 100644 index 0000000..7d93410 --- /dev/null +++ b/src/parser.c @@ -0,0 +1,668 @@ +/* + * Copyright (C) 2017-2019 emersion + * Copyright (C) 2019 cyclopsian + + * 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 X CONSORTIUM 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. + */ + +/* + * Parts of this file are taken from emersion/kanshi: + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/parser.c + * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h + */ + +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include + +#include "kanshi.h" + +enum kanshi_token_type { + KANSHI_TOKEN_LBRACKET, + KANSHI_TOKEN_RBRACKET, + KANSHI_TOKEN_STR, + KANSHI_TOKEN_NEWLINE, + KANSHI_TOKEN_COMMENT, +}; + +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; +}; +static char commentSign = '#'; + +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"; + case KANSHI_TOKEN_COMMENT: + return "comment"; + } + assert(0); +} + +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) { + 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 == '"') { + 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 ch; + } + + 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 == '"') { + parser->tok_type = KANSHI_TOKEN_STR; + parser->tok_str_len = 0; + return parser_read_quoted(parser); + } else if (ch == commentSign) { + parser->tok_type = KANSHI_TOKEN_COMMENT; + parser->tok_str_len = 0; + 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 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; + 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 (!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; + default: + assert(0); + } + 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 { + fprintf(stderr, + "unknown directive '%s' in profile output '%s'\n", + key_str, output->name); + return NULL; + } + } + break; + case KANSHI_TOKEN_NEWLINE: + return output; + case KANSHI_TOKEN_COMMENT: + parser_ignore_line(parser); + 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); + + // First parse an optional profile name + parser->tok_str_len = 0; + if (!parser_read_str(parser)) { + fprintf(stderr, "expected new profile, got %s\n", + token_type_str(parser->tok_type)); + return NULL; + } + profile->name = (parser->tok_str_len == 0) ? NULL : strdup(parser->tok_str); + + // Then parse the opening bracket + if (!parser_expect_token(parser, KANSHI_TOKEN_LBRACKET)) { + return NULL; + } + + // 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 + case KANSHI_TOKEN_COMMENT: + parser_ignore_line(parser); + break; // No-op + default: + fprintf(stderr, "unexpected %s in profile '%s'\n", + token_type_str(parser->tok_type), profile->name); + return NULL; + } + } +} + +static struct kanshi_config *_parse_config(struct kanshi_parser *parser) { + struct kanshi_config *config = calloc(1, sizeof(*config)); + wl_list_init(&config->profiles); + + while (1) { + int ch = parser_peek_char(parser); + if (ch < 0) { + return NULL; + } else if (ch == 0) { + return config; + } else if (ch == commentSign) { + parser_ignore_line(parser); + continue; + } else if (isspace(ch)) { + parser_read_char(parser); + continue; + } + + struct kanshi_profile *profile = parse_profile(parser); + if (!profile) { + return NULL; + } + + // Inset at the end to preserve ordering + wl_list_insert(config->profiles.prev, &profile->link); + } +} + +struct kanshi_config *kanshi_parse_config(const char *path) { + FILE *f = fopen(path, "r"); + if (f == NULL) { + return NULL; + } + + struct kanshi_parser parser = { + .f = f, + .next = -1, + .line = 1, + }; + + struct kanshi_config *config = _parse_config(&parser); + fclose(f); + if (config == NULL) { + fprintf(stderr, "failed to parse config file: " + "error on line %d, column %d\n", parser.line, parser.col); + return NULL; + } + + return config; +} + +static const char *transform_to_string(enum wl_output_transform transform) { + switch (transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + return "normal"; + case WL_OUTPUT_TRANSFORM_90: + return "90"; + case WL_OUTPUT_TRANSFORM_180: + return "180"; + case WL_OUTPUT_TRANSFORM_270: + return "270"; + case WL_OUTPUT_TRANSFORM_FLIPPED: + return "flipped"; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + return "flipped-90"; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return "flipped-180"; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return "flipped-270"; + } + return NULL; +} + +void kanshi_save_config(const char *path, struct kanshi_config *config) { + FILE *f = fopen(path, "w"); + if (f == NULL) { + fprintf(stderr, "fopen: %s: %s\n", path, strerror(errno)); + return; + } + + fprintf(f, "# DO NOT EDIT - file autogenerated by wdisplays\n\n"); + + struct kanshi_profile *profile; + wl_list_for_each(profile, &config->profiles, link) { + fprintf(f, "{\n"); + struct kanshi_profile_output *profile_output; + wl_list_for_each(profile_output, &profile->outputs, link) { + fprintf(f, "\toutput \"%s\"", profile_output->name); + if (profile_output->fields & KANSHI_OUTPUT_ENABLED) { + fprintf(f, " enable"); + if (profile_output->fields & KANSHI_OUTPUT_MODE) { + fprintf(f, " mode %dx%d@%0.3fHz", + profile_output->mode.width, profile_output->mode.height, + profile_output->mode.refresh / 1000.f); + } + if (profile_output->fields & KANSHI_OUTPUT_POSITION) { + fprintf(f, " position %d,%d", + profile_output->position.x, profile_output->position.y); + } + if (profile_output->fields & KANSHI_OUTPUT_SCALE) { + fprintf(f, " scale %f", profile_output->scale); + } + if (profile_output->fields & KANSHI_OUTPUT_TRANSFORM) { + fprintf(f, " transform %s", + transform_to_string(profile_output->transform)); + } + } else { + fprintf(f, " disable"); + } + fprintf(f, "\n"); + } + fprintf(f, "}\n\n"); + } + fclose(f); +} + +void kanshi_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); + if (profile->name != NULL) { + free(profile->name); + } + free(profile); + } + free(config); +} diff --git a/src/wdisplays.h b/src/wdisplays.h index e075461..6d20545 100644 --- a/src/wdisplays.h +++ b/src/wdisplays.h @@ -28,8 +28,8 @@ * https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h */ -#ifndef WDISPLAY_WDISPLAY_H -#define WDISPLAY_WDISPLAY_H +#ifndef WDISPLAYS_WDISPLAYS_H +#define WDISPLAYS_WDISPLAYS_H #define HEADS_MAX 64 #define HOVER_USECS (100 * 1000) @@ -250,7 +250,6 @@ struct wd_state { struct wd_render_data render; }; - /* * Creates the application state structure. */