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/3] 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/3] 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/3] 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;