From bec8d234850e9eb5dea5c7e0ecaf12b1ec9b8856 Mon Sep 17 00:00:00 2001 From: Shaochang Tan <478710209@qq.com> Date: Sun, 8 Oct 2023 09:09:22 +0200 Subject: [PATCH 1/7] add new feature: autosave config profile for kanshi. --- .vscode/settings.json | 7 ++ src/meson.build | 1 + src/outputs.c | 7 ++ src/store.c | 271 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 286 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 src/store.c diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dcb4ccb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "files.associations": { + "wdisplays.h": "c", + "wayland-client-protocol.h": "c", + "path": "c" + } +} \ No newline at end of file diff --git a/src/meson.build b/src/meson.build index 08830e9..8527452 100644 --- a/src/meson.build +++ b/src/meson.build @@ -20,6 +20,7 @@ executable( 'outputs.c', 'overlay.c', 'render.c', + 'store.c', resources, ], dependencies : [ diff --git a/src/outputs.c b/src/outputs.c index a5007e8..5108f61 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -28,6 +28,8 @@ #include "wlr-screencopy-unstable-v1-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" +extern int store_config(struct wl_list *outputs); + static void noop() { // This space is intentionally left blank } @@ -52,6 +54,11 @@ static void config_handle_succeeded(void *data, struct wd_pending_config *pending = data; zwlr_output_configuration_v1_destroy(config); wd_ui_apply_done(pending->state, pending->outputs); + if (store_config(pending->outputs) == 0) + { + wd_ui_show_error(pending->state, + "Change was applied successfully and config was saved."); + } destroy_pending(pending); } diff --git a/src/store.c b/src/store.c new file mode 100644 index 0000000..a2e1bf9 --- /dev/null +++ b/src/store.c @@ -0,0 +1,271 @@ +#include "wdisplays.h" +#include +#include +#include +#include +#include +#include +#define MAX_NAME_LENGTH 256 +#define MAX_MONITORS_NUM 10 +struct wd_head_config; +struct profile_line { + int start; + int end; +}; +char *get_config_file_path() { + // 获取用户的主目录路径 + const char *homeDir = getenv("HOME"); + + if (homeDir == NULL) { + perror("Cannot load $HOME env."); + return NULL; + } + + // 构建默认的配置文件路径 + char defaultPath[256]; // 假设文件路径不超过256个字符 + snprintf(defaultPath, sizeof(defaultPath), "%s/.config/kanshi/config", homeDir); + + // 尝试打开并读取 $HOME/.config/wdisplays/config 文件 + char wdisplaysPath[256]; + snprintf(wdisplaysPath, sizeof(wdisplaysPath), "%s/.config/wdisplays/config", homeDir); + + FILE *wdisplaysFile = fopen(wdisplaysPath, "r"); + if (wdisplaysFile != NULL) { + char line[256]; // 假设行的长度不超过256个字符 + + // 逐行读取文件,查找 "store PATH" 配置项 + while (fgets(line, sizeof(line), wdisplaysFile) != NULL) { + if (strstr(line, "store_path") != NULL) { + // 找到 "store PATH" 配置项,提取路径 + char *pathStart = strchr(line, '='); + if (pathStart != NULL) { + pathStart++; // 跳过等号 + char *pathEnd = strchr(pathStart, '\n'); + if (pathEnd != NULL) { + *pathEnd = '\0'; // 去除换行符 + fclose(wdisplaysFile); + return strdup(pathStart); // 返回提取的路径 + } + } + } + } + + fclose(wdisplaysFile); + } + + // 如果没有找到 "store PATH" 配置项,则返回默认路径 + return strdup(defaultPath); +} + +struct profile_line match(char **descriptions, int num, char *filename) { + struct profile_line matched_profile; + matched_profile.start = -1; + matched_profile.end = -1; + FILE *configFile = fopen(filename, "r"); + if (configFile == NULL) { + perror("File open failed."); + return matched_profile; + } + // 缓冲区用于存储文件行 + char buffer[1024]; + char profileName[MAX_NAME_LENGTH]; + int profileStartLine = 0; // 记录匹配到的profile的起始行号 + int profileEndLine = 0; // 记录匹配到的profile的结束行号 + + int lineCount = 0; // 用于记录当前行号 + + while (fgets(buffer, sizeof(buffer), configFile) != NULL) { + lineCount++; // 增加行号 + + // 检查是否包含 "profile" 关键字 + if (strstr(buffer, "profile") != NULL) { + // 从当前行提取 profile 名称 + sscanf(buffer, "profile %s {", profileName); + + // 标记当前 profile 是否匹配 + int profileMatched = 0; + + // 记录匹配到的profile的起始行号 + profileStartLine = lineCount; + + // 遍历 profile 中的输出行 + while (fgets(buffer, sizeof(buffer), configFile) != NULL) { + lineCount++; // 增加行号 + + // 检查是否到达当前 profile 的末尾 + if (buffer[0] == '}') { + // 记录匹配到的profile的结束行号 + profileEndLine = lineCount; + break; // 退出当前 profile + } + char outputName[MAX_NAME_LENGTH]; + // 从当前行提取输出名称 + char *trimmedBuffer = buffer; + while (isspace(*trimmedBuffer)) { + trimmedBuffer++; + } + sscanf(trimmedBuffer, "output \"%99[^\"]\"", outputName); + + // 检查是否匹配 + int matched = 0; + for (int i = 0; descriptions[i] != NULL; i++) { + if (strcmp(outputName, descriptions[i]) == 0) { + matched = 1; + profileMatched++; + break; + } + } + + if (!matched) { + // 如果有任何一个输出不匹配,则标记为不匹配 + profileMatched = 0; + break; + } + } + + if (profileMatched == num) { + printf("Matched profile:%s\n", profileName); + printf("Start line:%d\n", profileStartLine); + matched_profile.start = profileStartLine; + printf("End line:%d\n", profileEndLine); + matched_profile.end = profileEndLine; + + fclose(configFile); + return matched_profile; + } + } + } + + // 关闭配置文件 + fclose(configFile); + + printf("Cannot find exsiting profile to match\n"); + return matched_profile; +} + +int store_config(struct wl_list *outputs) { + char *file_name = get_config_file_path(); + char tmp_file_name[256]; + sprintf(tmp_file_name,"%s.tmp",file_name); + + char *descriptions[MAX_MONITORS_NUM]; + for (int i = 0; i < MAX_MONITORS_NUM; i++) { + descriptions[i] = NULL; + } + + char *outputConfigs[MAX_MONITORS_NUM]; + for (int i = 0; i < MAX_MONITORS_NUM; i++) { + outputConfigs[i] = (char *)malloc(MAX_NAME_LENGTH); + } + + struct wd_head_config *output; + int description_index = 0; + wl_list_for_each(output, outputs, link) { + struct wd_head *head = output->head; + + // for transform + char *trans_str = (char *)malloc(15 * sizeof(char)); + switch (output->transform) { + case WL_OUTPUT_TRANSFORM_NORMAL: + strcpy(trans_str, "normal"); + break; + case WL_OUTPUT_TRANSFORM_90: + strcpy(trans_str, "90"); + break; + case WL_OUTPUT_TRANSFORM_180: + strcpy(trans_str, "180"); + break; + case WL_OUTPUT_TRANSFORM_270: + strcpy(trans_str, "270"); + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + strcpy(trans_str, "flipped-90"); + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + strcpy(trans_str, "flipped-180"); + break; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + strcpy(trans_str, "flipped-270"); + break; + default: + strcpy(trans_str, "normal"); + break; + } + + if (description_index < MAX_MONITORS_NUM) { + descriptions[description_index] = strdup(head->description); + + sprintf( + outputConfigs[description_index], + "output \"%s\" position %d,%d mode %dx%d@%.4f scale %.2f transform %s", + head->description, output->x, output->y, output->width, + output->height, output->refresh / 1.0e3, output->scale, trans_str); + description_index++; + } else { + printf("Too many monitor!"); + return 1; + } + + free(trans_str); + } + + int num_of_monitors = description_index; + + struct profile_line matched_profile; + matched_profile = match(descriptions, num_of_monitors, file_name); + + if (matched_profile.start == -1) { + FILE *file = fopen(file_name, "a"); + if (file == NULL) { + perror("File open failed."); + return 1; + } + fprintf(file, "\nprofile {\n"); + for (int i = 0; i= matched_profile.start && _line < matched_profile.end - 1) { + if(_i_output>=num_of_monitors){ + perror("Null pointer"); + fclose(tmp); + fclose(file); + return 1; + } + fprintf(tmp," %s\n",outputConfigs[_i_output]); + free(outputConfigs[_i_output]); + + _i_output++; + } else{ + fprintf(tmp,"%s",_buffer); + } + _line++; + } + fclose(file); + fclose(tmp); + + remove(file_name); + rename(tmp_file_name, file_name); + } + + return 0; +} \ No newline at end of file From 4411521f8ad1ed4df6c1bb003251ec1645c90704 Mon Sep 17 00:00:00 2001 From: Shaochang Tan <478710209@qq.com> Date: Sun, 8 Oct 2023 09:21:01 +0200 Subject: [PATCH 2/7] update readme --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 79bc825..b406db8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Compositors that are known to support the protocol are [Sway] and [Wayfire]. The goal of this project is to allow precise adjustment of display settings in kiosks, digital signage, and other elaborate multi-monitor setups. + ![Screenshot](wdisplays.png) # Installation @@ -70,8 +71,16 @@ It's intended to be the Wayland equivalent of an xrandr GUI, like [ARandR]. Sway, like i3, doesn't save any settings unless you put them in the config file. See man `sway-output`. If you want to have multiple configurations depending on the monitors connected, you'll need to use an external program -like [kanshi] or [way-displays]. Integration with that and other external -daemons is planned. +like [kanshi] or [way-displays]. + +When you apply a new change, the setting will be defaultly added to $HOME/.config/kanshi/config, +if there is already a profile for the same monitors combination, the change will be applied on +existing one. +you can add kanshi autostart to your sway config: +``` +exec_always pkill kanshi +exec_always kanshi +``` ### How do I add support to my compositor? From d5f0e48443c8aac4357cd411b03f143f23df30ac Mon Sep 17 00:00:00 2001 From: Shaochang Tan <478710209@qq.com> Date: Fri, 31 May 2024 20:45:27 +0200 Subject: [PATCH 3/7] 1. remove vscode settings 2. check $XDG_CONFIG_HOME before $HOME 3. all MAX SIZE now use platform-based macro from limits.h rather than magic number --- .gitignore | 1 + .vscode/settings.json | 7 --- README.md | 4 +- src/store.c | 124 ++++++++++++++++++++++-------------------- 4 files changed, 69 insertions(+), 67 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index 48835df..2122d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /build/ +.vscode *.user diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index dcb4ccb..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "files.associations": { - "wdisplays.h": "c", - "wayland-client-protocol.h": "c", - "path": "c" - } -} \ No newline at end of file diff --git a/README.md b/README.md index b406db8..9949216 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,8 @@ if there is already a profile for the same monitors combination, the change will existing one. you can add kanshi autostart to your sway config: ``` -exec_always pkill kanshi -exec_always kanshi +exec kanshi +exec_always kanshictl reload ``` ### How do I add support to my compositor? diff --git a/src/store.c b/src/store.c index a2e1bf9..6dab784 100644 --- a/src/store.c +++ b/src/store.c @@ -5,6 +5,8 @@ #include #include #include +#include +#include #define MAX_NAME_LENGTH 256 #define MAX_MONITORS_NUM 10 struct wd_head_config; @@ -13,47 +15,50 @@ struct profile_line { int end; }; char *get_config_file_path() { - // 获取用户的主目录路径 - const char *homeDir = getenv("HOME"); - - if (homeDir == NULL) { - perror("Cannot load $HOME env."); - return NULL; + char defaultPath[PATH_MAX]; // platform based marco PATH_MAX + char wdisplaysPath[PATH_MAX]; + // if $XDG_CONFIG_HOME is set, use it + { + const char *configDir = getenv("XDG_CONFIG_HOME"); + char defaultConfigDir[PATH_MAX]; + if (configDir == NULL) { + const char *homeDir = getenv("HOME"); + if (homeDir == NULL) { + perror("Cannot find home directory"); + return NULL; + } + snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s/.config", homeDir); + } else { + snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s", configDir); + } + snprintf(defaultPath, sizeof(defaultPath), "%s/kanshi/config", defaultConfigDir); + snprintf(wdisplaysPath, sizeof(wdisplaysPath), "%s/wdisplays/config", defaultConfigDir); } - // 构建默认的配置文件路径 - char defaultPath[256]; // 假设文件路径不超过256个字符 - snprintf(defaultPath, sizeof(defaultPath), "%s/.config/kanshi/config", homeDir); - - // 尝试打开并读取 $HOME/.config/wdisplays/config 文件 - char wdisplaysPath[256]; - snprintf(wdisplaysPath, sizeof(wdisplaysPath), "%s/.config/wdisplays/config", homeDir); - FILE *wdisplaysFile = fopen(wdisplaysPath, "r"); if (wdisplaysFile != NULL) { - char line[256]; // 假设行的长度不超过256个字符 + char line[LINE_MAX]; // LINE_MAX is a platform based marco - // 逐行读取文件,查找 "store PATH" 配置项 + // try to match "store_path" term while (fgets(line, sizeof(line), wdisplaysFile) != NULL) { if (strstr(line, "store_path") != NULL) { - // 找到 "store PATH" 配置项,提取路径 + // if found, extract path char *pathStart = strchr(line, '='); if (pathStart != NULL) { - pathStart++; // 跳过等号 + pathStart++; // skip '=' char *pathEnd = strchr(pathStart, '\n'); if (pathEnd != NULL) { - *pathEnd = '\0'; // 去除换行符 + *pathEnd = '\0'; // replace '\n' with '\0' fclose(wdisplaysFile); - return strdup(pathStart); // 返回提取的路径 + return strdup(pathStart); // return path } } } } - fclose(wdisplaysFile); } - // 如果没有找到 "store PATH" 配置项,则返回默认路径 + // if store_path is not found in wdisplays config file, return default path return strdup(defaultPath); } @@ -61,73 +66,72 @@ struct profile_line match(char **descriptions, int num, char *filename) { struct profile_line matched_profile; matched_profile.start = -1; matched_profile.end = -1; + // -1 means not found FILE *configFile = fopen(filename, "r"); if (configFile == NULL) { perror("File open failed."); return matched_profile; } - // 缓冲区用于存储文件行 - char buffer[1024]; + // buffer to store each line + char buffer[LINE_MAX]; char profileName[MAX_NAME_LENGTH]; - int profileStartLine = 0; // 记录匹配到的profile的起始行号 - int profileEndLine = 0; // 记录匹配到的profile的结束行号 + int profileStartLine = 0; // mark the start line of matched profile + int profileEndLine = 0; // mark the end line of matched profile - int lineCount = 0; // 用于记录当前行号 + int lineCount = 0; // current line number while (fgets(buffer, sizeof(buffer), configFile) != NULL) { - lineCount++; // 增加行号 + lineCount++; - // 检查是否包含 "profile" 关键字 + // check if "profile" keyword is in the line if (strstr(buffer, "profile") != NULL) { - // 从当前行提取 profile 名称 + // extract profile name sscanf(buffer, "profile %s {", profileName); - // 标记当前 profile 是否匹配 - int profileMatched = 0; + // the number of matched outputs + uint32_t profileMatchedNum = 0; - // 记录匹配到的profile的起始行号 + // record the start line of the profile profileStartLine = lineCount; - // 遍历 profile 中的输出行 while (fgets(buffer, sizeof(buffer), configFile) != NULL) { - lineCount++; // 增加行号 + lineCount++; - // 检查是否到达当前 profile 的末尾 + // check if the profile ends if (buffer[0] == '}') { - // 记录匹配到的profile的结束行号 profileEndLine = lineCount; - break; // 退出当前 profile + break; } char outputName[MAX_NAME_LENGTH]; // 从当前行提取输出名称 char *trimmedBuffer = buffer; while (isspace(*trimmedBuffer)) { - trimmedBuffer++; + trimmedBuffer++; // skip leading spaces } - sscanf(trimmedBuffer, "output \"%99[^\"]\"", outputName); + sscanf(trimmedBuffer, "output \"%99[^\"]\"", outputName); // extract output name - // 检查是否匹配 - int matched = 0; + // check if the output name is in the descriptions + bool matched = false; for (int i = 0; descriptions[i] != NULL; i++) { if (strcmp(outputName, descriptions[i]) == 0) { - matched = 1; - profileMatched++; + matched = true; + profileMatchedNum++; break; } } if (!matched) { - // 如果有任何一个输出不匹配,则标记为不匹配 - profileMatched = 0; + // if any output is not matched, break + profileMatchedNum = 0; break; } } - if (profileMatched == num) { - printf("Matched profile:%s\n", profileName); - printf("Start line:%d\n", profileStartLine); + if (profileMatchedNum == num) { + printf("Matched profile:%s\n", profileName); + printf("Start line:%d\n", profileStartLine); matched_profile.start = profileStartLine; - printf("End line:%d\n", profileEndLine); + printf("End line:%d\n", profileEndLine); matched_profile.end = profileEndLine; fclose(configFile); @@ -136,16 +140,14 @@ struct profile_line match(char **descriptions, int num, char *filename) { } } - // 关闭配置文件 fclose(configFile); - - printf("Cannot find exsiting profile to match\n"); + printf("Cannot find existing profile to match\n"); return matched_profile; } int store_config(struct wl_list *outputs) { char *file_name = get_config_file_path(); - char tmp_file_name[256]; + char tmp_file_name[PATH_MAX]; sprintf(tmp_file_name,"%s.tmp",file_name); char *descriptions[MAX_MONITORS_NUM]; @@ -194,7 +196,7 @@ int store_config(struct wl_list *outputs) { if (description_index < MAX_MONITORS_NUM) { descriptions[description_index] = strdup(head->description); - + // write output config in given format sprintf( outputConfigs[description_index], "output \"%s\" position %d,%d mode %dx%d@%.4f scale %.2f transform %s", @@ -202,7 +204,8 @@ int store_config(struct wl_list *outputs) { output->height, output->refresh / 1.0e3, output->scale, trans_str); description_index++; } else { - printf("Too many monitor!"); + free(trans_str); + printf("Too many monitor! 10 is the"); return 1; } @@ -215,9 +218,11 @@ int store_config(struct wl_list *outputs) { matched_profile = match(descriptions, num_of_monitors, file_name); if (matched_profile.start == -1) { + // append new profile FILE *file = fopen(file_name, "a"); if (file == NULL) { perror("File open failed."); + free(file_name); return 1; } fprintf(file, "\nprofile {\n"); @@ -228,19 +233,21 @@ int store_config(struct wl_list *outputs) { fprintf(file, "}"); fclose(file); } else if (matched_profile.start < matched_profile.end) { - // rewrite correspondece lines + // rewrite correspondence lines FILE *file = fopen(file_name, "r"); if (file == NULL) { perror("File open failed."); + free(file_name); return 1; } FILE *tmp = fopen(tmp_file_name, "w"); if (tmp == NULL) { perror("Tmp file cannot be created."); fclose(file); + free(file_name); return 1; } - char _buffer[1024]; + char _buffer[LINE_MAX]; int _line = 0; int _i_output = 0; while (fgets(_buffer, sizeof(_buffer), file) != NULL) { @@ -265,6 +272,7 @@ int store_config(struct wl_list *outputs) { remove(file_name); rename(tmp_file_name, file_name); + free(file_name); } return 0; From 828086008ad2a29634889c80f28d1060729b3bdd Mon Sep 17 00:00:00 2001 From: Stefan Zipproth Date: Fri, 27 Jun 2025 16:34:55 +0200 Subject: [PATCH 4/7] Fix kanshi compatibility by using output names instead of descriptions Kanshi fails to match profiles when output descriptions contain parentheses like 'Iiyama North America PL2294H2 1207823601758 (DP-3)'. According to kanshi(5) manual, valid output criteria are: - Output names (e.g. 'DP-1') - Manufacturer/Model/Serial without parentheses This changes store.c to use head->name (e.g. 'DP-3') instead of head->description, making kanshi profiles work correctly. Fixes 'no profile matched' error when kanshi tries to apply configurations. --- src/store.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/store.c b/src/store.c index 6dab784..21306a6 100644 --- a/src/store.c +++ b/src/store.c @@ -195,12 +195,12 @@ int store_config(struct wl_list *outputs) { } if (description_index < MAX_MONITORS_NUM) { - descriptions[description_index] = strdup(head->description); + descriptions[description_index] = strdup(head->name); // write output config in given format sprintf( outputConfigs[description_index], - "output \"%s\" position %d,%d mode %dx%d@%.4f scale %.2f transform %s", - head->description, output->x, output->y, output->width, + "output %s position %d,%d mode %dx%d@%.4f scale %.2f transform %s", + head->name, output->x, output->y, output->width, output->height, output->refresh / 1.0e3, output->scale, trans_str); description_index++; } else { From 402b3fc6689de28abed518425a0a41526890c185 Mon Sep 17 00:00:00 2001 From: Stefan Zipproth Date: Fri, 27 Jun 2025 22:41:40 +0200 Subject: [PATCH 5/7] Fix profile matching for unquoted output syntax The match() function was still using the old quoted output syntax ("output \"%99[^\"]\"" pattern) which fails to match profiles written with the new unquoted syntax (output DP-3). This caused duplicate profiles to be created instead of updating existing ones. Now supports both quoted (legacy) and unquoted (current) output formats for backward compatibility. --- src/store.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/store.c b/src/store.c index 21306a6..2425ff6 100644 --- a/src/store.c +++ b/src/store.c @@ -108,7 +108,25 @@ struct profile_line match(char **descriptions, int num, char *filename) { while (isspace(*trimmedBuffer)) { trimmedBuffer++; // skip leading spaces } - sscanf(trimmedBuffer, "output \"%99[^\"]\"", outputName); // extract output name + char tempName[MAX_NAME_LENGTH]; + int matched_scan = 0; + + // Try quoted format first (legacy): output "Long Description (DP-3)" + if (sscanf(trimmedBuffer, "output \"%255[^\"]\"", tempName) == 1) { + // Extract output name from parentheses if present: (DP-3) -> DP-3 + char *paren_start = strrchr(tempName, '('); + char *paren_end = strrchr(tempName, ')'); + if (paren_start && paren_end && paren_end > paren_start) { + size_t len = paren_end - paren_start - 1; + strncpy(outputName, paren_start + 1, len); + outputName[len] = '\0'; + matched_scan = 1; + } + } else if (sscanf(trimmedBuffer, "output %99s", outputName) == 1) { + // Try unquoted format: output DP-3 + matched_scan = 1; + } + if (matched_scan != 1) continue; // Skip unparseable lines // check if the output name is in the descriptions bool matched = false; @@ -276,4 +294,4 @@ int store_config(struct wl_list *outputs) { } return 0; -} \ No newline at end of file +} From 4455950c9f6ff456334c81b13e81a0d69957b71e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20Andr=C3=A9=20Charles=20Gantner?= Date: Tue, 1 Jul 2025 00:40:16 +0200 Subject: [PATCH 6/7] PR#11 improvment proposal (#1) * wip * Fix deprecated flag * check config path only on startup * use HEADS_MAX from wdisplays.h * remove useless strcpy * update readme and meson syntax * clang-format * Fix C23 issues gcc 15 is more strict and compiles C by default with C23. `noop` is used as a default null initializer, but now it must have the correct type, so add a cast. See also: https://bugs.gentoo.org/946954 --------- Co-authored-by: Viorel Munteanu --- .clang-format | 57 +++++++- CHANGELOG.md | 8 ++ protocol/meson.build | 2 +- src/main.c | 2 +- src/meson.build | 2 +- src/outputs.c | 10 +- src/store.c | 320 +++++++++++++++++++++---------------------- src/wdisplays.h | 20 ++- 8 files changed, 245 insertions(+), 176 deletions(-) diff --git a/.clang-format b/.clang-format index 912692d..a981890 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,55 @@ --- -IndentWidth: '2' - -... +Language: Cpp +IndentWidth: 2 +AlignConsecutiveAssignments: + Enabled: true + AcrossEmptyLines: false + AcrossComments: true + AlignCompound: true + PadOperators: true +AlignConsecutiveBitFields: Consecutive +AlignConsecutiveMacros: Consecutive +AlignConsecutiveShortCaseStatements: + Enabled: true + # AlignCaseArrows: true + AlignCaseColons: true +AlignOperands: Align +AlignTrailingComments: Always +AllowShortBlocksOnASingleLine: Always +AllowShortCaseLabelsOnASingleLine: true +# AllowShortCaseExpressionOnASingleLine: true +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: AllIfsAndElse +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: true +BinPackArguments: true +BinPackParameters: true +BreakBeforeBraces: Attach +BreakBeforeConceptDeclarations: Never +BreakBeforeBinaryOperators: true +BreakBeforeTernaryOperators: true +ColumnLimit: 128 +IndentCaseBlocks: false +IndentCaseLabels: true +IndentPPDirectives: AfterHash +InsertBraces: false +InsertNewlineAtEOF: true +LineEnding: LF +PointerAlignment: Right +QualifierAlignment: Left +ReferenceAlignment: Right +ReflowComments: true +RemoveBracesLLVM: true +SeparateDefinitionBlocks: Always +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeParens: ControlStatementsExceptControlMacros +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpacesInAngles: Never +SpacesInLineCommentPrefix: + Minimum: 1 + Maximum: 1 +SpacesInParens: Never +UseTab: Never diff --git a/CHANGELOG.md b/CHANGELOG.md index 3803a77..86d5c59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ This project tries to adhere to [Semantic Versioning][2]. ## [Unreleased] +### Added + +Support for saving kanshi config file + +### Changed + +### Fixed + ## [1.1.1] - 2023-07-01 ### Added diff --git a/protocol/meson.build b/protocol/meson.build index 5f34a7a..670a4af 100644 --- a/protocol/meson.build +++ b/protocol/meson.build @@ -5,7 +5,7 @@ wayland_scanner = find_program('wayland-scanner') wayland_client = dependency('wayland-client') wayland_protos = dependency('wayland-protocols', version: '>=1.17') -wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') +wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') wayland_scanner_code = generator( wayland_scanner, diff --git a/src/main.c b/src/main.c index e9690c4..a81e348 100644 --- a/src/main.c +++ b/src/main.c @@ -1083,7 +1083,7 @@ static void activate(GtkApplication* app, gpointer user_data) { int main(int argc, char *argv[]) { g_setenv("GDK_GL", "gles", FALSE); - GtkApplication *app = gtk_application_new(WDISPLAYS_APP_ID, G_APPLICATION_FLAGS_NONE); + GtkApplication *app = gtk_application_new(WDISPLAYS_APP_ID, G_APPLICATION_DEFAULT_FLAGS); g_signal_connect(app, "activate", G_CALLBACK(activate), NULL); int status = g_application_run(G_APPLICATION(app), argc, argv); g_object_unref(app); diff --git a/src/meson.build b/src/meson.build index 8527452..286f631 100644 --- a/src/meson.build +++ b/src/meson.build @@ -6,7 +6,7 @@ m_dep = cc.find_library('m', required : false) rt_dep = cc.find_library('rt', required : false) gdk = dependency('gdk-3.0', version: '>= 3.24') gtk = dependency('gtk+-3.0', version: '>= 3.24') -assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') +assert(gdk.get_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') epoxy = dependency('epoxy') configure_file(input: 'config.h.in', output: 'config.h', configuration: conf) diff --git a/src/outputs.c b/src/outputs.c index 5108f61..f4ac15c 100644 --- a/src/outputs.c +++ b/src/outputs.c @@ -533,7 +533,7 @@ static void output_manager_handle_done(void *data, static const struct zwlr_output_manager_v1_listener output_manager_listener = { .head = output_manager_handle_head, .done = output_manager_handle_done, - .finished = noop, + .finished = (void (*)(void *, struct zwlr_output_manager_v1 *))noop, }; static void registry_handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { @@ -560,7 +560,7 @@ static void registry_handle_global(void *data, struct wl_registry *registry, static const struct wl_registry_listener registry_listener = { .global = registry_handle_global, - .global_remove = noop, + .global_remove = (void (*)(void *, struct wl_registry *, uint32_t))noop, }; void wd_add_output_management_listener(struct wd_state *state, struct @@ -610,10 +610,10 @@ static void output_name(void *data, struct zxdg_output_v1 *zxdg_output_v1, static const struct zxdg_output_v1_listener output_listener = { .logical_position = output_logical_position, - .logical_size = noop, - .done = noop, + .logical_size = (void (*)(void *, struct zxdg_output_v1 *, int32_t, int32_t))noop, + .done = (void (*)(void *, struct zxdg_output_v1 *))noop, .name = output_name, - .description = noop + .description = (void (*)(void *, struct zxdg_output_v1 *, const char *))noop }; void wd_add_output(struct wd_state *state, struct wl_output *wl_output, diff --git a/src/store.c b/src/store.c index 6dab784..f2c43c2 100644 --- a/src/store.c +++ b/src/store.c @@ -1,164 +1,181 @@ #include "wdisplays.h" #include +#include #include +#include #include #include #include #include -#include -#include + #define MAX_NAME_LENGTH 256 -#define MAX_MONITORS_NUM 10 + struct wd_head_config; + struct profile_line { int start; int end; }; -char *get_config_file_path() { - char defaultPath[PATH_MAX]; // platform based marco PATH_MAX - char wdisplaysPath[PATH_MAX]; - // if $XDG_CONFIG_HOME is set, use it - { - const char *configDir = getenv("XDG_CONFIG_HOME"); - char defaultConfigDir[PATH_MAX]; + +typedef enum { Looking_for_profile, Looking_for_outputs, Found } parser_states; + +char *wd_get_config_file_path() { + char kanshiConfigPath[PATH_MAX]; + char wdisplaysPath[PATH_MAX]; + char defaultConfigDir[PATH_MAX]; + // if $XDG_CONFIG_HOME is set, use it + { + char *configDir = getenv("XDG_CONFIG_HOME"); + if (configDir == NULL) { // fallback to $HOME + configDir = getenv("HOME"); if (configDir == NULL) { - const char *homeDir = getenv("HOME"); - if (homeDir == NULL) { - perror("Cannot find home directory"); - return NULL; - } - snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s/.config", homeDir); - } else { - snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s", configDir); + dprintf(2, "%s:%i:%s(): Cannot find $XDG_CONFIG_HOME nor $HOME directories", __FILE__, __LINE__, __func__); + return NULL; + } else { // configdir is $HOME/config + snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s/.config", configDir); } - snprintf(defaultPath, sizeof(defaultPath), "%s/kanshi/config", defaultConfigDir); - snprintf(wdisplaysPath, sizeof(wdisplaysPath), "%s/wdisplays/config", defaultConfigDir); + } else { // configDir is $XDG_CONFIG_HOME + snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s", configDir); } + } - FILE *wdisplaysFile = fopen(wdisplaysPath, "r"); - if (wdisplaysFile != NULL) { - char line[LINE_MAX]; // LINE_MAX is a platform based marco + // set default kanshi config path + snprintf(kanshiConfigPath, sizeof(kanshiConfigPath), "%s/kanshi/config", defaultConfigDir); - // try to match "store_path" term - while (fgets(line, sizeof(line), wdisplaysFile) != NULL) { - if (strstr(line, "store_path") != NULL) { - // if found, extract path - char *pathStart = strchr(line, '='); - if (pathStart != NULL) { - pathStart++; // skip '=' - char *pathEnd = strchr(pathStart, '\n'); - if (pathEnd != NULL) { - *pathEnd = '\0'; // replace '\n' with '\0' - fclose(wdisplaysFile); - return strdup(pathStart); // return path - } - } - } - } - fclose(wdisplaysFile); - } + // look for store_path in wdisplays.conf + snprintf(wdisplaysPath, sizeof(wdisplaysPath), "%s/wdisplays.conf", defaultConfigDir); + + FILE *wdisplaysFile = fopen(wdisplaysPath, "r"); + if (wdisplaysFile != NULL) { + char line[LINE_MAX]; // LINE_MAX is a platform-dependendant macro + + // try to match "store_path" term + while (fgets(line, sizeof(line), wdisplaysFile) != NULL) { + if (strstr(line, "store_path") != NULL) { + // if found, extract path + char *pathStart = strchr(line, '='); + if (pathStart != NULL) { + pathStart++; // skip '=' + while (isspace(*pathStart)) pathStart++; // skip spaces between '=' and the start of the path + char *pathEnd = strchr(pathStart, '\n'); + size_t pathLen; + if (pathEnd != NULL) pathLen = pathEnd - pathStart; + else // store_path= is the last line and there's no newline at the end of the file + pathLen = strnlen(pathStart, PATH_MAX); + // save path + strncpy(kanshiConfigPath, pathStart, pathLen); + } else + ; // store_path was not followed by an equal sign on this line + } else + ; // this line does not contain store_path + } // reached end of file + fclose(wdisplaysFile); + } else { // can't open config file + dprintf(2, "%s:%i:%s(): Can't open %s : ", __FILE__, __LINE__, __func__, wdisplaysPath); + perror(NULL); + } - // if store_path is not found in wdisplays config file, return default path - return strdup(defaultPath); + // look for WDISPLAYS_KANSHI_CONFIG + { + char *envKanshiConf = getenv("WDISPLAYS_KANSHI_CONFIG"); + if (envKanshiConf != NULL) strncpy(kanshiConfigPath, envKanshiConf, sizeof(kanshiConfigPath)); + else + ; + } + char *finalPath = strndup(kanshiConfigPath, PATH_MAX); + if (finalPath == NULL) { + dprintf(2, "%s:%i:%s(): ", __FILE__, __LINE__, __func__); + perror("Failed to allocate memory for kanshi config path"); + } + return finalPath; } -struct profile_line match(char **descriptions, int num, char *filename) { +struct profile_line match(char **descriptions, int num, const char *filename) { struct profile_line matched_profile; matched_profile.start = -1; - matched_profile.end = -1; + matched_profile.end = -1; // -1 means not found - FILE *configFile = fopen(filename, "r"); + FILE *configFile = fopen(filename, "r"); if (configFile == NULL) { - perror("File open failed."); + dprintf(2, "%s:%i:%s(): Can't open %s : ", __FILE__, __LINE__, __func__, filename); + perror(NULL); return matched_profile; } // buffer to store each line char buffer[LINE_MAX]; - char profileName[MAX_NAME_LENGTH]; + char *profileName; int profileStartLine = 0; // mark the start line of matched profile - int profileEndLine = 0; // mark the end line of matched profile + int profileEndLine = 0; // mark the end line of matched profile - int lineCount = 0; // current line number - - while (fgets(buffer, sizeof(buffer), configFile) != NULL) { + int lineCount = 0; // current line number + uint32_t profileMatchedNum = 0; // current number of matched outputs + parser_states ps = Looking_for_profile; // current state of the parser + while (ps != Found && fgets(buffer, sizeof(buffer), configFile) != NULL) { lineCount++; + switch (ps) { + case Found: break; // unreachable code - // check if "profile" keyword is in the line - if (strstr(buffer, "profile") != NULL) { - // extract profile name - sscanf(buffer, "profile %s {", profileName); - - // the number of matched outputs - uint32_t profileMatchedNum = 0; - - // record the start line of the profile - profileStartLine = lineCount; - - while (fgets(buffer, sizeof(buffer), configFile) != NULL) { - lineCount++; + case Looking_for_profile:; + // check if "profile" keyword is in the line and remember its position + char *pstart = strstr(buffer, "profile "); + if (pstart != NULL) { + pstart += 7; + char *pend = strchr(pstart, '{'); // find the end of the profile name + while (isspace(*pend)) pend--; + size_t pnsize = pend - pstart; + // use strndup to extract it without being size constrained + profileName = strndup(pstart, pnsize); + // record the start line of the profile + profileStartLine = lineCount; + ps = Looking_for_outputs; + } + break; + case Looking_for_outputs: // check if the profile ends if (buffer[0] == '}') { profileEndLine = lineCount; - break; - } - char outputName[MAX_NAME_LENGTH]; - // 从当前行提取输出名称 - char *trimmedBuffer = buffer; - while (isspace(*trimmedBuffer)) { - trimmedBuffer++; // skip leading spaces - } - sscanf(trimmedBuffer, "output \"%99[^\"]\"", outputName); // extract output name - - // check if the output name is in the descriptions - bool matched = false; - for (int i = 0; descriptions[i] != NULL; i++) { - if (strcmp(outputName, descriptions[i]) == 0) { - matched = true; + if (profileMatchedNum == num) ps = Found; + } else { + char *on_start = strstr(buffer, "output"); + on_start = strchr(on_start, '"'); + on_start++; + char *on_end = strchr(on_start, '"'); + char *outputName = strndup(on_start, on_end - on_start); + // check if the output name is in the descriptions + int i = 0; + while (descriptions[i] != NULL && strcmp(outputName, descriptions[i])) i++; + if (descriptions[i] != NULL) { profileMatchedNum++; - break; + } else { + // if any output is not matched, break + profileMatchedNum = 0; + ps = Looking_for_profile; } } - - if (!matched) { - // if any output is not matched, break - profileMatchedNum = 0; - break; - } - } - - if (profileMatchedNum == num) { - printf("Matched profile:%s\n", profileName); - printf("Start line:%d\n", profileStartLine); - matched_profile.start = profileStartLine; - printf("End line:%d\n", profileEndLine); - matched_profile.end = profileEndLine; - - fclose(configFile); - return matched_profile; - } + break; } } - fclose(configFile); - printf("Cannot find existing profile to match\n"); + if (ps == Found) { + printf("Matched profile:%s\n", profileName); + printf("Start line:%d\nEnd line:%d\n", profileStartLine, profileEndLine); + matched_profile.start = profileStartLine; + matched_profile.end = profileEndLine; + } else dprintf(2, "%s:%i:%s(): Cannot find existing profile to match\n", __FILE__, __LINE__, __func__); return matched_profile; } -int store_config(struct wl_list *outputs) { - char *file_name = get_config_file_path(); +int wd_store_config(struct wl_list *outputs) { + const char *file_name = wd_get_kanshi_config(); char tmp_file_name[PATH_MAX]; - sprintf(tmp_file_name,"%s.tmp",file_name); + sprintf(tmp_file_name, "%s.tmp", file_name); - char *descriptions[MAX_MONITORS_NUM]; - for (int i = 0; i < MAX_MONITORS_NUM; i++) { - descriptions[i] = NULL; - } + char *descriptions[HEADS_MAX]; + for (int i = 0; i < HEADS_MAX; i++) descriptions[i] = NULL; - char *outputConfigs[MAX_MONITORS_NUM]; - for (int i = 0; i < MAX_MONITORS_NUM; i++) { - outputConfigs[i] = (char *)malloc(MAX_NAME_LENGTH); - } + char *outputConfigs[HEADS_MAX]; + for (int i = 0; i < HEADS_MAX; i++) outputConfigs[i] = (char *)malloc(MAX_NAME_LENGTH); struct wd_head_config *output; int description_index = 0; @@ -166,50 +183,29 @@ int store_config(struct wl_list *outputs) { struct wd_head *head = output->head; // for transform - char *trans_str = (char *)malloc(15 * sizeof(char)); + char *trans_str; switch (output->transform) { - case WL_OUTPUT_TRANSFORM_NORMAL: - strcpy(trans_str, "normal"); - break; - case WL_OUTPUT_TRANSFORM_90: - strcpy(trans_str, "90"); - break; - case WL_OUTPUT_TRANSFORM_180: - strcpy(trans_str, "180"); - break; - case WL_OUTPUT_TRANSFORM_270: - strcpy(trans_str, "270"); - break; - case WL_OUTPUT_TRANSFORM_FLIPPED_90: - strcpy(trans_str, "flipped-90"); - break; - case WL_OUTPUT_TRANSFORM_FLIPPED_180: - strcpy(trans_str, "flipped-180"); - break; - case WL_OUTPUT_TRANSFORM_FLIPPED_270: - strcpy(trans_str, "flipped-270"); - break; - default: - strcpy(trans_str, "normal"); - break; - } + case WL_OUTPUT_TRANSFORM_NORMAL : trans_str = "normal"; + case WL_OUTPUT_TRANSFORM_90 : trans_str = "90"; + case WL_OUTPUT_TRANSFORM_180 : trans_str = "180"; + case WL_OUTPUT_TRANSFORM_270 : trans_str = "270"; + case WL_OUTPUT_TRANSFORM_FLIPPED_90 : trans_str = "flipped-90"; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: trans_str = "flipped-180"; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: trans_str = "flipped-270"; + default : trans_str = "normal"; + }; - if (description_index < MAX_MONITORS_NUM) { + if (description_index < HEADS_MAX) { descriptions[description_index] = strdup(head->description); // write output config in given format - sprintf( - outputConfigs[description_index], - "output \"%s\" position %d,%d mode %dx%d@%.4f scale %.2f transform %s", - head->description, output->x, output->y, output->width, - output->height, output->refresh / 1.0e3, output->scale, trans_str); + sprintf(outputConfigs[description_index], "output \"%s\" position %d,%d mode %dx%d@%.4f scale %.2f transform %s", + head->description, output->x, output->y, output->width, output->height, output->refresh / 1.0e3, output->scale, + trans_str); description_index++; } else { - free(trans_str); - printf("Too many monitor! 10 is the"); + dprintf(2, "Too many monitor!\n\t%i is the maximum allowed number", HEADS_MAX); return 1; } - - free(trans_str); } int num_of_monitors = description_index; @@ -221,59 +217,57 @@ int store_config(struct wl_list *outputs) { // append new profile FILE *file = fopen(file_name, "a"); if (file == NULL) { - perror("File open failed."); - free(file_name); + dprintf(2, "%s:%i:%s(): Can't open %s : ", __FILE__, __LINE__, __func__, file_name); + perror(NULL); return 1; } fprintf(file, "\nprofile {\n"); - for (int i = 0; i= matched_profile.start && _line < matched_profile.end - 1) { - if(_i_output>=num_of_monitors){ + if (_i_output >= num_of_monitors) { perror("Null pointer"); fclose(tmp); fclose(file); return 1; } - fprintf(tmp," %s\n",outputConfigs[_i_output]); + fprintf(tmp, " %s\n", outputConfigs[_i_output]); free(outputConfigs[_i_output]); _i_output++; - } else{ - fprintf(tmp,"%s",_buffer); + } else { + fprintf(tmp, "%s", _buffer); } _line++; } fclose(file); fclose(tmp); - + remove(file_name); rename(tmp_file_name, file_name); - free(file_name); } return 0; -} \ No newline at end of file +} diff --git a/src/wdisplays.h b/src/wdisplays.h index 4824017..e13f975 100644 --- a/src/wdisplays.h +++ b/src/wdisplays.h @@ -14,7 +14,7 @@ #include "config.h" -#define HEADS_MAX 64 +#define HEADS_MAX 64 #define HOVER_USECS (100 * 1000) #include @@ -110,10 +110,12 @@ struct wd_head { bool enabled; struct wd_mode *mode; + struct { int32_t width, height; int32_t refresh; } custom_mode; + int32_t x, y; enum wl_output_transform transform; double scale; @@ -224,7 +226,6 @@ struct wd_state { struct wd_render_data render; }; - /* * Creates the application state structure. */ @@ -339,4 +340,19 @@ void wd_redraw_overlay(struct wd_output *output); */ void wd_destroy_overlay(struct wd_output *output); +/* + * Locate kanshi config + */ +char *wd_get_config_file_path(); + +/* + * Returns kanshi config path + */ +char *wd_get_kanshi_config(); + +/* + * Updates kanshi config + */ +int wd_store_config(struct wl_list *outputs); + #endif From 25211d7327c95748775f41017f78a92245f53c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jason=20Andr=C3=A9=20Charles=20Gantner?= Date: Tue, 8 Jul 2025 02:10:16 +0200 Subject: [PATCH 7/7] Add licensing header(s) (#3) * Add partial licensing header * add licensing info where needed * Update SPDX header in store.c --------- Co-authored-by: TanShaochang <30321432+petertheprocess@users.noreply.github.com> --- .clang-format | 6 +++++- src/store.c | 4 ++++ src/wdisplays.h | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.clang-format b/.clang-format index a981890..a249c32 100644 --- a/.clang-format +++ b/.clang-format @@ -1,6 +1,9 @@ --- -Language: Cpp IndentWidth: 2 +# SPDX-SnippetBegin +# SPDX-License-Identifier: CC0-1.0 +# SPDX-SnippetCopyrightText: NONE +Language: Cpp AlignConsecutiveAssignments: Enabled: true AcrossEmptyLines: false @@ -53,3 +56,4 @@ SpacesInLineCommentPrefix: Maximum: 1 SpacesInParens: Never UseTab: Never +# SPDX-SnippetEnd diff --git a/src/store.c b/src/store.c index f2c43c2..5d07272 100644 --- a/src/store.c +++ b/src/store.c @@ -1,3 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-FileCopyrightText: 2024-2025 Shaochang Tan +// SPDX-FileCopyrightText: 2024-2025 Jason André Charles Gantner + #include "wdisplays.h" #include #include diff --git a/src/wdisplays.h b/src/wdisplays.h index e13f975..6ed7605 100644 --- a/src/wdisplays.h +++ b/src/wdisplays.h @@ -340,6 +340,9 @@ 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 /* * Locate kanshi config */ @@ -354,5 +357,5 @@ char *wd_get_kanshi_config(); * Updates kanshi config */ int wd_store_config(struct wl_list *outputs); - +// SPDX-SnippetEnd #endif