Add integration with kanshi daemon
This commit is contained in:
parent
a3d3d13a01
commit
e208957fa2
2
meson_options.txt
Normal file
2
meson_options.txt
Normal file
@ -0,0 +1,2 @@
|
||||
option('kanshi', type: 'feature', value: 'auto', description: 'Enable integration with the kanshi daemon')
|
||||
|
95
src/kanshi.h
Normal file
95
src/kanshi.h
Normal file
@ -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 <stdbool.h>
|
||||
#include <wayland-client.h>
|
||||
|
||||
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
|
@ -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
|
||||
)
|
||||
|
132
src/outputs.c
132
src/outputs.c
@ -27,7 +27,6 @@
|
||||
* https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@ -40,7 +39,12 @@
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <glib.h>
|
||||
|
||||
#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));
|
||||
|
668
src/parser.c
Normal file
668
src/parser.c
Normal file
@ -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 <assert.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#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),
|
||||
"<anonymous at line %d, col %d>", parser->line, parser->col);
|
||||
if (ret >= 0) {
|
||||
profile->name = strdup(generated_name);
|
||||
} else {
|
||||
profile->name = strdup("<anonymous>");
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user