diff --git a/.gitignore b/.gitignore index 48835df..2122d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /build/ +.vscode *.user diff --git a/README.md b/README.md index 79bc825..9949216 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 kanshi +exec_always kanshictl reload +``` ### How do I add support to my compositor? 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..6dab784 --- /dev/null +++ b/src/store.c @@ -0,0 +1,279 @@ +#include "wdisplays.h" +#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]; + 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); + } + + FILE *wdisplaysFile = fopen(wdisplaysPath, "r"); + if (wdisplaysFile != NULL) { + char line[LINE_MAX]; // LINE_MAX is a platform based marco + + // 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); + } + + // if store_path is not found in wdisplays config file, return default 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; + // -1 means not found + FILE *configFile = fopen(filename, "r"); + if (configFile == NULL) { + perror("File open failed."); + return matched_profile; + } + // buffer to store each line + char buffer[LINE_MAX]; + char profileName[MAX_NAME_LENGTH]; + int profileStartLine = 0; // mark the start 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) { + lineCount++; + + // 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++; + + // 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; + profileMatchedNum++; + break; + } + } + + 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; + } + } + } + + fclose(configFile); + 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[PATH_MAX]; + 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); + // 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); + description_index++; + } else { + free(trans_str); + printf("Too many monitor! 10 is the"); + 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) { + // 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"); + 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); + free(file_name); + } + + return 0; +} \ No newline at end of file