Merge branch 'kanshi-config'

This commit is contained in:
Mars-Wave
2026-09-18 18:53:34 +02:00
11 changed files with 948 additions and 24 deletions
+3
View File
@@ -8,4 +8,7 @@
#define WDISPLAYS_VERSION "@version@"
#define WDISPLAYS_RESOURCE_PREFIX "@resource_prefix@"
// Features
#mesondefine WITH_KANSHI
#endif
+42 -3
View File
@@ -55,10 +55,12 @@ static gboolean send_apply(gpointer data) {
struct wl_display *wl_display = gdk_wayland_display_get_wl_display(display);
wd_apply_state(state, outputs, wl_display);
state->apply_pending = FALSE;
state->apply_manual = false;
return FALSE;
}
static void apply_state(struct wd_state *state) {
static void apply_state(struct wd_state *state, bool manual) {
state->apply_manual = state->apply_manual || manual;
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title");
if (!state->autoapply) {
gtk_style_context_add_class(gtk_widget_get_style_context(state->spinner), "visible");
@@ -365,7 +367,7 @@ static void show_apply(struct wd_state *state) {
const gchar *page = "title";
if (has_changes(state)) {
if (state->autoapply) {
apply_state(state);
apply_state(state, false);
} else {
page = "apply";
}
@@ -469,6 +471,10 @@ void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs) {
}
void wd_ui_show_error(struct wd_state *state, const char *message) {
if (gtk_widget_in_destruction(gtk_widget_get_toplevel(state->info_bar))) {
fprintf(stderr, "%s\n", message);
return;
}
gtk_label_set_text(GTK_LABEL(state->info_label), message);
gtk_widget_show(state->info_bar);
gtk_info_bar_set_revealed(GTK_INFO_BAR(state->info_bar), TRUE);
@@ -487,6 +493,9 @@ static void cleanup(GtkWidget *window, gpointer data) {
g_object_unref(state->grabbing_cursor);
g_object_unref(state->move_cursor);
g_clear_object(&state->settings);
#ifdef WITH_KANSHI
wd_save_kanshi_snapshot(state);
#endif
wd_state_destroy(state);
}
@@ -899,7 +908,7 @@ static void cancel_changes(GSimpleAction *action, GVariant *param, gpointer data
}
static void apply_changes(GSimpleAction *action, GVariant *param, gpointer data) {
apply_state(data);
apply_state(data, true);
}
static void info_response(GtkInfoBar *info_bar, gint response_id, gpointer data) {
@@ -921,6 +930,11 @@ static void auto_apply_selected(GSimpleAction *action, GVariant *param, gpointer
}
state->autoapply = g_variant_get_boolean(param);
g_simple_action_set_state(action, param);
#ifdef WITH_KANSHI
if (!state->autoapply) {
wd_save_kanshi_snapshot(state);
}
#endif
}
static gboolean redraw_canvas(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer data) {
@@ -958,6 +972,18 @@ static void overlay_selected(GSimpleAction *action, GVariant *param, gpointer da
}
}
#ifdef WITH_KANSHI
static void save_kanshi_config_selected(GSimpleAction *action, GVariant *param, gpointer data) {
struct wd_state *state = data;
if (state->settings != NULL) {
g_settings_set_boolean(state->settings, "save-kanshi-config", g_variant_get_boolean(param));
}
state->save_kanshi_config = g_variant_get_boolean(param);
wd_drop_kanshi_snapshot(state);
g_simple_action_set_state(action, param);
}
#endif
static void window_state_changed(GtkWidget *window, GdkEventWindowState *event,
gpointer data) {
struct wd_state *state = data;
@@ -1150,6 +1176,9 @@ static void activate(GtkApplication* app, gpointer user_data) {
state->autoapply = g_settings_get_boolean(state->settings, "auto-apply");
state->capture = g_settings_get_boolean(state->settings, "capture-screens");
state->show_overlay = g_settings_get_boolean(state->settings, "show-overlay");
#ifdef WITH_KANSHI
state->save_kanshi_config = g_settings_get_boolean(state->settings, "save-kanshi-config");
#endif
}
if (startup->autoapply != -1) {
state->autoapply = startup->autoapply;
@@ -1177,10 +1206,20 @@ static void activate(GtkApplication* app, gpointer user_data) {
g_signal_connect(overlay_action, "change-state", G_CALLBACK(overlay_selected), state);
g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(overlay_action));
#ifdef WITH_KANSHI
action = g_simple_action_new_stateful("save-kanshi-config", NULL,
g_variant_new_boolean(state->save_kanshi_config));
g_signal_connect(action, "change-state", G_CALLBACK(save_kanshi_config_selected), state);
g_action_map_add_action(G_ACTION_MAP(main_actions), G_ACTION(action));
#endif
GMenu *main_menu = g_menu_new();
g_menu_append(main_menu, "_Automatically Apply Changes", "app.auto-apply");
g_menu_append(main_menu, "_Show Screen Contents", "app.capture-screens");
g_menu_append(main_menu, "_Overlay Screen Names", "app.show-overlay");
#ifdef WITH_KANSHI
g_menu_append(main_menu, "Save to _kanshi Config", "app.save-kanshi-config");
#endif
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(state->menu_button), G_MENU_MODEL(main_menu));
gtk_menu_button_set_use_popover(GTK_MENU_BUTTON(state->menu_button), false);
+15 -9
View File
@@ -11,17 +11,23 @@ epoxy = dependency('epoxy')
configure_file(input: 'config.h.in', output: 'config.h', configuration: conf)
sources = [
'main.c',
'glviewport.c',
'headform.c',
'outputs.c',
'overlay.c',
'render.c',
resources,
]
if get_option('kanshi').allowed()
sources += 'store.c'
endif
executable(
'wdisplays',
[
'main.c',
'glviewport.c',
'headform.c',
'outputs.c',
'overlay.c',
'render.c',
resources,
],
sources,
dependencies : [
m_dep,
rt_dep,
+92 -4
View File
@@ -35,23 +35,60 @@ static void noop() {
struct wd_pending_config {
struct wd_state *state;
struct wl_list *outputs;
struct wl_list link;
bool manual;
bool incomplete;
};
static void destroy_pending(struct wd_pending_config *pending) {
static void destroy_outputs(struct wl_list *outputs) {
struct wd_head_config *output, *tmp;
wl_list_for_each_safe(output, tmp, pending->outputs, link) {
wl_list_for_each_safe(output, tmp, outputs, link) {
wl_list_remove(&output->link);
free(output);
}
free(pending->outputs);
free(outputs);
}
static void destroy_pending(struct wd_pending_config *pending) {
wl_list_remove(&pending->link);
if (pending->outputs != NULL) {
destroy_outputs(pending->outputs);
}
free(pending);
}
#ifdef WITH_KANSHI
void wd_drop_kanshi_snapshot(struct wd_state *state) {
if (state->kanshi_snapshot != NULL) {
destroy_outputs(state->kanshi_snapshot);
state->kanshi_snapshot = NULL;
}
}
void wd_save_kanshi_snapshot(struct wd_state *state) {
if (state->kanshi_snapshot != NULL) {
wd_store_config(state, state->kanshi_snapshot);
wd_drop_kanshi_snapshot(state);
}
}
#endif
static void config_handle_succeeded(void *data,
struct zwlr_output_configuration_v1 *config) {
struct wd_pending_config *pending = data;
zwlr_output_configuration_v1_destroy(config);
wd_ui_apply_done(pending->state, pending->outputs);
#ifdef WITH_KANSHI
if (pending->state->save_kanshi_config && !pending->incomplete) {
wd_drop_kanshi_snapshot(pending->state);
if (pending->manual) {
wd_store_config(pending->state, pending->outputs);
} else {
pending->state->kanshi_snapshot = pending->outputs;
pending->outputs = NULL;
}
}
#endif
destroy_pending(pending);
}
@@ -90,6 +127,8 @@ void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs,
struct wd_pending_config *pending = calloc(1, sizeof(*pending));
pending->state = state;
pending->outputs = new_outputs;
pending->manual = state->apply_manual;
wl_list_insert(&state->pending_configs, &pending->link);
zwlr_output_configuration_v1_add_listener(config, &config_listener, pending);
@@ -395,6 +434,9 @@ static void wd_head_destroy(struct wd_head *head) {
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);
}
@@ -420,6 +462,9 @@ static void mode_handle_preferred(void *data,
static void mode_handle_finished(void *data,
struct zwlr_output_mode_v1 *wlr_mode) {
struct wd_mode *mode = data;
#ifdef WITH_KANSHI
wd_save_kanshi_snapshot(mode->head->state);
#endif
if (mode->head->mode == mode) {
mode->head->mode = NULL;
}
@@ -517,10 +562,46 @@ static void head_handle_scale(void *data,
wd_ui_reset_head(head, WD_FIELD_SCALE);
}
static void head_handle_make(void *data,
struct zwlr_output_head_v1 *wlr_head, const char *make) {
struct wd_head *head = data;
head->make = strdup(make);
}
static void head_handle_model(void *data,
struct zwlr_output_head_v1 *wlr_head, const char *model) {
struct wd_head *head = data;
head->model = strdup(model);
}
static void head_handle_serial_number(void *data,
struct zwlr_output_head_v1 *wlr_head, const char *serial_number) {
struct wd_head *head = data;
head->serial_number = strdup(serial_number);
}
static void remove_pending_head(struct wd_state *state, struct wd_head *head) {
struct wd_pending_config *pending;
wl_list_for_each(pending, &state->pending_configs, link) {
struct wd_head_config *output, *tmp;
wl_list_for_each_safe(output, tmp, pending->outputs, link) {
if (output->head == head) {
wl_list_remove(&output->link);
free(output);
pending->incomplete = true;
}
}
}
}
static void head_handle_finished(void *data,
struct zwlr_output_head_v1 *wlr_head) {
struct wd_head *head = data;
struct wd_state *state = head->state;
#ifdef WITH_KANSHI
wd_save_kanshi_snapshot(state);
#endif
remove_pending_head(state, head);
wl_list_remove(&head->link);
wd_head_destroy(head);
@@ -548,12 +629,18 @@ static const struct zwlr_output_head_v1_listener head_listener = {
.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,
};
static void output_manager_handle_head(void *data,
struct zwlr_output_manager_v1 *manager,
struct zwlr_output_head_v1 *wlr_head) {
struct wd_state *state = data;
#ifdef WITH_KANSHI
wd_save_kanshi_snapshot(state);
#endif
struct wd_head *head = calloc(1, sizeof(*head));
head->state = state;
@@ -596,7 +683,7 @@ static void registry_handle_global(void *data, struct wl_registry *registry,
if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) {
state->output_manager = wl_registry_bind(registry, name,
&zwlr_output_manager_v1_interface, 1);
&zwlr_output_manager_v1_interface, MIN(version, 2));
zwlr_output_manager_v1_add_listener(state->output_manager,
&output_manager_listener, state);
} else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
@@ -737,6 +824,7 @@ struct wd_state *wd_state_create(void) {
state->show_overlay = true;
wl_list_init(&state->heads);
wl_list_init(&state->outputs);
wl_list_init(&state->pending_configs);
wl_list_init(&state->render.heads);
return state;
}
+673
View File
@@ -0,0 +1,673 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2024-2025 Shaochang Tan
// SPDX-FileCopyrightText: 2024-2025 Jason André Charles Gantner
#include <errno.h>
#include <fnmatch.h>
#include <string.h>
#include <wordexp.h>
#include "wdisplays.h"
#define INCLUDE_DEPTH_MAX 16
struct kanshi_file {
char *path;
char *text;
GPtrArray *dirs;
};
struct kanshi_directive {
char *name;
GPtrArray *params;
GPtrArray *children;
size_t start, name_start, end;
size_t criteria_start, criteria_end;
struct kanshi_file *file;
};
struct kanshi_config {
GPtrArray *files;
GPtrArray *profiles;
GPtrArray *outputs;
};
struct kanshi_parser {
const char *text;
size_t pos;
int line;
};
static char *get_config_path(void) {
const char *env_path = g_getenv("WDISPLAYS_KANSHI_CONFIG");
if (env_path != NULL && env_path[0] != '\0') {
return g_strdup(env_path);
}
return g_build_filename(g_get_user_config_dir(), "kanshi", "config", NULL);
}
static void kanshi_directive_free(gpointer data) {
struct kanshi_directive *dir = data;
g_free(dir->name);
g_ptr_array_unref(dir->params);
if (dir->children != NULL) {
g_ptr_array_unref(dir->children);
}
g_free(dir);
}
static char peek(struct kanshi_parser *parser) {
return parser->text[parser->pos];
}
static void skip_blanks(struct kanshi_parser *parser) {
while (peek(parser) == ' ' || peek(parser) == '\t') {
parser->pos++;
}
}
static void next_line(struct kanshi_parser *parser) {
while (peek(parser) != '\0' && peek(parser) != '\n') {
parser->pos++;
}
if (peek(parser) == '\n') {
parser->pos++;
parser->line++;
}
}
static bool parse_error(struct kanshi_parser *parser, GError **error,
const char *message) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"line %d: %s", parser->line, message);
return false;
}
static bool parse_word(struct kanshi_parser *parser, GString *word,
GError **error) {
char quote = peek(parser);
if (quote == '"' || quote == '\'') {
parser->pos++;
} else {
quote = '\0';
}
while (true) {
char c = peek(parser);
if (quote != '\0' && c == quote) {
parser->pos++;
return true;
}
if (c == '\0' || c == '\n') {
if (quote != '\0') {
return parse_error(parser, error, "unterminated quoted string");
}
return true;
}
if (quote == '\0' && (c == ' ' || c == '\t')) {
return true;
}
if (quote == '\0' && strchr("\"'{}", c) != NULL) {
return parse_error(parser, error, "unexpected character in word");
}
if (c == '\\' && quote != '\'') {
parser->pos++;
c = peek(parser);
if (c == '\0' || c == '\n') {
return parse_error(parser, error, "cannot escape a line break");
}
}
g_string_append_c(word, c);
parser->pos++;
}
}
static GPtrArray *parse_block(struct kanshi_parser *parser, bool nested,
GError **error);
static struct kanshi_directive *parse_directive(struct kanshi_parser *parser,
GError **error) {
struct kanshi_directive *dir = g_new0(struct kanshi_directive, 1);
dir->params = g_ptr_array_new_with_free_func(g_free);
dir->name_start = parser->pos;
dir->start = parser->pos;
while (dir->start > 0 && (parser->text[dir->start - 1] == ' '
|| parser->text[dir->start - 1] == '\t')) {
dir->start--;
}
if (dir->start > 0 && parser->text[dir->start - 1] != '\n') {
dir->start = dir->name_start;
}
g_autoptr(GString) word = g_string_new(NULL);
if (!parse_word(parser, word, error)) {
goto err;
}
dir->name = g_strdup(word->str);
skip_blanks(parser);
while (peek(parser) != '\0' && peek(parser) != '\n') {
if (peek(parser) == '{') {
parser->pos++;
skip_blanks(parser);
if (peek(parser) != '\n') {
parse_error(parser, error, "expected a line break after '{'");
goto err;
}
next_line(parser);
dir->children = parse_block(parser, true, error);
if (dir->children == NULL) {
goto err;
}
dir->end = parser->pos;
skip_blanks(parser);
if (peek(parser) != '\0' && peek(parser) != '\n') {
parser->pos = dir->end;
return dir;
}
break;
}
if (peek(parser) == '}') {
parse_error(parser, error, "unexpected '}'");
goto err;
}
size_t param_start = parser->pos;
g_string_truncate(word, 0);
if (!parse_word(parser, word, error)) {
goto err;
}
if (dir->params->len == 0) {
dir->criteria_start = param_start;
dir->criteria_end = parser->pos;
}
g_ptr_array_add(dir->params, g_strdup(word->str));
skip_blanks(parser);
}
next_line(parser);
dir->end = parser->pos;
return dir;
err:
kanshi_directive_free(dir);
return NULL;
}
static GPtrArray *parse_block(struct kanshi_parser *parser, bool nested,
GError **error) {
GPtrArray *dirs = g_ptr_array_new_with_free_func(kanshi_directive_free);
while (true) {
skip_blanks(parser);
char c = peek(parser);
if (c == '\n' || c == '#') {
next_line(parser);
} else if (c == '\0') {
if (nested) {
parse_error(parser, error, "expected '}'");
break;
}
return dirs;
} else if (c == '}') {
if (!nested) {
parse_error(parser, error, "unexpected '}'");
break;
}
parser->pos++;
return dirs;
} else {
struct kanshi_directive *dir = parse_directive(parser, error);
if (dir == NULL) {
break;
}
g_ptr_array_add(dirs, dir);
}
}
g_ptr_array_unref(dirs);
return NULL;
}
static const char *find_param(struct kanshi_directive *dir, const char *key) {
for (guint i = 1; i + 1 < dir->params->len; i++) {
if (strcmp(g_ptr_array_index(dir->params, i), key) == 0) {
return g_ptr_array_index(dir->params, i + 1);
}
}
for (guint i = 0; dir->children != NULL && i < dir->children->len; i++) {
struct kanshi_directive *child = g_ptr_array_index(dir->children, i);
if (strcmp(child->name, key) == 0 && child->params->len > 0) {
return g_ptr_array_index(child->params, 0);
}
}
return NULL;
}
static bool is_output(struct kanshi_directive *dir) {
return strcmp(dir->name, "output") == 0 && dir->params->len > 0;
}
static char *head_identifier(struct wd_head *head) {
return g_strdup_printf("%s %s %s",
head->make != NULL ? head->make : "Unknown",
head->model != NULL ? head->model : "Unknown",
head->serial_number != NULL ? head->serial_number : "Unknown");
}
static bool criteria_match(const char *criteria, struct wd_head *head) {
g_autofree char *identifier = head_identifier(head);
return strcmp(criteria, "*") == 0 || strcmp(criteria, head->name) == 0
|| fnmatch(criteria, identifier, 0) == 0;
}
static const char *resolve_alias(struct kanshi_config *config,
const char *criteria) {
if (criteria[0] != '$') {
return criteria;
}
for (guint i = 0; i < config->outputs->len; i++) {
struct kanshi_directive *dir = g_ptr_array_index(config->outputs, i);
const char *alias = find_param(dir, "alias");
if (alias != NULL && strcmp(alias, criteria) == 0) {
return g_ptr_array_index(dir->params, 0);
}
}
return NULL;
}
static bool match_profile(struct kanshi_config *config,
struct kanshi_directive *profile,
struct wd_head_config **heads, int num_heads,
struct kanshi_directive **matches) {
g_autoptr(GPtrArray) outputs = g_ptr_array_new();
for (guint i = 0; i < profile->children->len; i++) {
struct kanshi_directive *child = g_ptr_array_index(profile->children, i);
if (!is_output(child)) {
continue;
}
if (strcmp(g_ptr_array_index(child->params, 0), "*") == 0) {
g_ptr_array_add(outputs, child);
} else {
g_ptr_array_insert(outputs, 0, child);
}
}
if (outputs->len != (guint) num_heads) {
return false;
}
memset(matches, 0, num_heads * sizeof(*matches));
for (guint i = 0; i < outputs->len; i++) {
struct kanshi_directive *output = g_ptr_array_index(outputs, i);
const char *criteria = resolve_alias(config,
g_ptr_array_index(output->params, 0));
if (criteria == NULL) {
return false;
}
int j = 0;
while (j < num_heads
&& (matches[j] != NULL || !criteria_match(criteria, heads[j]->head))) {
j++;
}
if (j == num_heads) {
return false;
}
matches[j] = output;
}
return true;
}
static void append_word(GString *str, const char *word) {
if (word[0] != '\0' && strpbrk(word, " \t\"'{}\\#") == NULL) {
g_string_append(str, word);
return;
}
g_string_append_c(str, '"');
for (const char *c = word; *c != '\0'; c++) {
if (*c == '"' || *c == '\\') {
g_string_append_c(str, '\\');
}
g_string_append_c(str, *c);
}
g_string_append_c(str, '"');
}
static void append_criteria(GString *str, struct wd_head_config **heads,
int num_heads, struct wd_head *head) {
g_autofree char *identifier = head_identifier(head);
bool usable = head->make != NULL || head->model != NULL
|| head->serial_number != NULL;
if (identifier[0] == '$' || strchr(identifier, '\n') != NULL) {
usable = false;
}
for (int i = 0; usable && i < num_heads; i++) {
g_autofree char *other = head_identifier(heads[i]->head);
if (heads[i]->head != head && strcmp(identifier, other) == 0) {
usable = false;
}
}
if (!usable) {
append_word(str, head->name);
return;
}
g_autoptr(GString) pattern = g_string_new(NULL);
for (const char *c = identifier; *c != '\0'; c++) {
if (strchr("*?[\\", *c) != NULL) {
g_string_append_c(pattern, '\\');
}
g_string_append_c(pattern, *c);
}
append_word(str, pattern->str);
}
static const char *transform_name(enum wl_output_transform transform) {
switch (transform) {
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";
default:
return "normal";
}
}
static bool is_custom_mode(struct wd_head_config *output) {
struct wd_mode *mode;
wl_list_for_each(mode, &output->head->modes, link) {
if (mode->width == output->width && mode->height == output->height
&& mode->refresh == output->refresh) {
return false;
}
}
return true;
}
static void format_scale(char *buf, size_t size, double scale) {
wl_fixed_t fixed = wl_fixed_from_double(scale);
scale = wl_fixed_to_double(fixed);
for (int digits = 0; digits <= 8; digits++) {
char format[8];
snprintf(format, sizeof(format), "%%.%df", digits);
g_ascii_formatd(buf, size, format, scale);
if (wl_fixed_from_double(g_ascii_strtod(buf, NULL)) == fixed) {
break;
}
}
}
static void append_settings(GString *str, struct wd_head_config *output,
const char *adaptive_sync) {
if (!output->enabled) {
g_string_append(str, " disable");
} else {
g_string_append(str, " enable mode ");
if (is_custom_mode(output)) {
g_string_append(str, "--custom ");
}
g_string_append_printf(str, "%dx%d", output->width, output->height);
if (output->refresh > 0) {
char refresh[G_ASCII_DTOSTR_BUF_SIZE];
g_ascii_formatd(refresh, sizeof(refresh), "%.3f", output->refresh / 1000.);
g_string_append_printf(str, "@%sHz", refresh);
}
char scale[G_ASCII_DTOSTR_BUF_SIZE];
format_scale(scale, sizeof(scale), output->scale);
g_string_append_printf(str, " position %d,%d scale %s transform %s",
output->x, output->y, scale, transform_name(output->transform));
}
if (adaptive_sync != NULL) {
g_string_append(str, " adaptive_sync ");
append_word(str, adaptive_sync);
}
g_string_append_c(str, '\n');
}
static void append_comments(GString *str, const char *text,
struct kanshi_directive *dir) {
const char *end = text + dir->end;
const char *line = strchr(text + dir->name_start, '\n');
while (line != NULL && ++line < end) {
const char *next = strchr(line, '\n');
if (line[strspn(line, " \t")] == '#') {
g_string_append_len(str, line, (next != NULL ? next + 1 : end) - line);
}
line = next;
}
}
static void rewrite_profile(GString *str, const char *text,
struct kanshi_directive *profile, struct wd_head_config **heads,
int num_heads, struct kanshi_directive **matches) {
size_t pos = 0;
for (guint i = 0; i < profile->children->len; i++) {
struct kanshi_directive *child = g_ptr_array_index(profile->children, i);
g_string_append_len(str, text + pos, child->start - pos);
pos = child->end;
if (!is_output(child)) {
g_string_append_len(str, text + child->start, child->end - child->start);
continue;
}
for (int j = 0; j < num_heads; j++) {
if (matches[j] == child) {
append_comments(str, text, child);
g_string_append_len(str, text + child->start,
child->name_start - child->start);
g_string_append(str, "output ");
g_string_append_len(str, text + child->criteria_start,
child->criteria_end - child->criteria_start);
append_settings(str, heads[j], find_param(child, "adaptive_sync"));
}
}
}
g_string_append(str, text + pos);
}
static void append_profile(GString *str, struct wd_head_config **heads,
int num_heads) {
if (str->len > 0 && str->str[str->len - 1] != '\n') {
g_string_append_c(str, '\n');
}
if (str->len > 0) {
g_string_append_c(str, '\n');
}
g_string_append(str, "profile {\n");
for (int i = 0; i < num_heads; i++) {
g_string_append(str, "\toutput ");
append_criteria(str, heads, num_heads, heads[i]->head);
append_settings(str, heads[i], NULL);
}
g_string_append(str, "}\n");
}
static void kanshi_file_free(gpointer data) {
struct kanshi_file *file = data;
g_free(file->path);
g_free(file->text);
if (file->dirs != NULL) {
g_ptr_array_unref(file->dirs);
}
g_free(file);
}
static bool load_file(struct kanshi_config *config, const char *path,
bool included, int depth, GError **error);
static bool load_include(struct kanshi_config *config,
struct kanshi_directive *dir, int depth, GError **error) {
if (dir->params->len != 1) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
"%s: include expects exactly one path", dir->file->path);
return false;
}
const char *pattern = g_ptr_array_index(dir->params, 0);
wordexp_t words;
int ret = wordexp(pattern, &words, WRDE_NOCMD | WRDE_UNDEF);
if (ret != 0) {
if (ret == WRDE_NOSPACE) {
wordfree(&words);
}
g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"%s: cannot expand include %s", dir->file->path, pattern);
return false;
}
bool ok = true;
for (size_t i = 0; ok && i < words.we_wordc; i++) {
if (!g_path_is_absolute(words.we_wordv[i])) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"%s: include %s is relative to the directory kanshi runs in",
dir->file->path, words.we_wordv[i]);
ok = false;
} else {
ok = load_file(config, words.we_wordv[i], true, depth + 1, error);
}
}
wordfree(&words);
return ok;
}
static bool load_file(struct kanshi_config *config, const char *path,
bool included, int depth, GError **error) {
if (depth > INCLUDE_DEPTH_MAX) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_TOO_MANY_LINKS,
"%s: includes nested too deeply", path);
return false;
}
struct kanshi_file *file = g_new0(struct kanshi_file, 1);
file->path = g_strdup(path);
g_ptr_array_add(config->files, file);
if (!g_file_get_contents(path, &file->text, NULL, error)) {
if (included || !g_error_matches(*error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
return false;
}
g_clear_error(error);
file->text = g_strdup("");
}
struct kanshi_parser parser = { .text = file->text, .line = 1 };
file->dirs = parse_block(&parser, false, error);
if (file->dirs == NULL) {
g_prefix_error(error, "%s: ", path);
return false;
}
for (guint i = 0; i < file->dirs->len; i++) {
struct kanshi_directive *dir = g_ptr_array_index(file->dirs, i);
dir->file = file;
if (strcmp(dir->name, "profile") == 0 && dir->children != NULL) {
g_ptr_array_add(config->profiles, dir);
} else if (is_output(dir)) {
g_ptr_array_add(config->outputs, dir);
} else if (strcmp(dir->name, "include") == 0
&& !load_include(config, dir, depth, error)) {
return false;
}
}
return true;
}
static char *update_config(struct kanshi_config *config,
struct wl_list *outputs, struct kanshi_file **file, GError **error) {
struct wd_head_config *heads[HEADS_MAX];
int num_heads = 0;
struct wd_head_config *output;
wl_list_for_each_reverse(output, outputs, link) {
if (num_heads == HEADS_MAX) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "too many outputs");
return NULL;
}
heads[num_heads++] = output;
}
if (num_heads == 0) {
g_set_error(error, G_IO_ERROR, G_IO_ERROR_FAILED, "no outputs");
return NULL;
}
GString *str = g_string_new(NULL);
struct kanshi_directive *matches[HEADS_MAX];
for (guint i = 0; i < config->profiles->len; i++) {
struct kanshi_directive *profile = g_ptr_array_index(config->profiles, i);
if (match_profile(config, profile, heads, num_heads, matches)) {
*file = profile->file;
rewrite_profile(str, (*file)->text, profile, heads, num_heads, matches);
return g_string_free(str, FALSE);
}
}
*file = g_ptr_array_index(config->files, 0);
g_string_append(str, (*file)->text);
append_profile(str, heads, num_heads);
return g_string_free(str, FALSE);
}
static char *resolve_links(const char *path) {
char *current = g_strdup(path);
for (int i = 0; i < 40; i++) {
char *target = g_file_read_link(current, NULL);
if (target == NULL) {
break;
}
if (!g_path_is_absolute(target)) {
g_autofree char *dir = g_path_get_dirname(current);
g_autofree char *relative = target;
target = g_build_filename(dir, relative, NULL);
}
g_free(current);
current = target;
}
return current;
}
static bool write_config(const char *config_path, const char *contents,
GError **error) {
g_autofree char *path = resolve_links(config_path);
g_autofree char *dir = g_path_get_dirname(path);
if (g_mkdir_with_parents(dir, 0755) != 0) {
int err = errno;
g_set_error(error, G_FILE_ERROR, g_file_error_from_errno(err),
"%s: %s", dir, g_strerror(err));
return false;
}
return g_file_set_contents(path, contents, -1, error);
}
static void reload_kanshi(void) {
g_autofree char *kanshictl = g_find_program_in_path("kanshictl");
if (kanshictl == NULL) {
return;
}
char *argv[] = { kanshictl, "reload", NULL };
g_autoptr(GError) error = NULL;
if (!g_spawn_async(NULL, argv, NULL, 0, NULL, NULL, NULL, &error)) {
fprintf(stderr, "Failed to run kanshictl: %s\n", error->message);
}
}
void wd_store_config(struct wd_state *state, struct wl_list *outputs) {
g_autofree char *path = get_config_path();
g_autoptr(GPtrArray) files = g_ptr_array_new_with_free_func(kanshi_file_free);
g_autoptr(GPtrArray) profiles = g_ptr_array_new();
g_autoptr(GPtrArray) global_outputs = g_ptr_array_new();
struct kanshi_config config = { files, profiles, global_outputs };
struct kanshi_file *file = NULL;
g_autofree char *updated = NULL;
g_autoptr(GError) error = NULL;
if (!load_file(&config, path, false, 0, &error)) {
goto err;
}
updated = update_config(&config, outputs, &file, &error);
if (updated == NULL || !write_config(file->path, updated, &error)) {
goto err;
}
reload_kanshi();
return;
err:;
g_autofree char *message = g_strdup_printf(
"Could not save the kanshi config: %s", error->message);
wd_ui_show_error(state, message);
}
+28
View File
@@ -108,6 +108,7 @@ struct wd_head {
uint32_t id;
char *name, *description;
char *make, *model, *serial_number;
int32_t phys_width, phys_height; // mm
struct wl_list modes;
@@ -187,12 +188,16 @@ struct wd_state {
struct wl_shm *shm;
struct wl_list heads;
struct wl_list outputs;
struct wl_list pending_configs;
uint32_t serial;
bool apply_pending;
bool apply_manual;
bool autoapply;
bool capture;
bool show_overlay;
bool save_kanshi_config;
struct wl_list *kanshi_snapshot;
double zoom;
unsigned int apply_idle;
@@ -280,6 +285,19 @@ void wd_add_output_management_listener(struct wd_state *state, struct wl_display
*/
void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs, struct wl_display *display);
#ifdef WITH_KANSHI
/*
* Saves the layout of the last auto-apply to the kanshi config, unless it
* has been saved or dropped already.
*/
void wd_save_kanshi_snapshot(struct wd_state *state);
/*
* Forgets the layout of the last auto-apply without saving it.
*/
void wd_drop_kanshi_snapshot(struct wd_state *state);
#endif
/*
* Queues capture of the next frame of all screens.
*/
@@ -354,4 +372,14 @@ void wd_redraw_overlay(struct wd_output *output);
*/
void wd_destroy_overlay(struct wd_output *output);
// SPDX-SnippetBegin
// SPDX-License-Identifier: MIT
// SPDX-SnippetCopyrightText: 2024-2025 Jason André Charles Gantner
#ifdef WITH_KANSHI
/*
* Updates kanshi config
*/
void wd_store_config(struct wd_state *state, struct wl_list *outputs);
#endif
// SPDX-SnippetEnd
#endif