diff --git a/.clang-format b/.clang-format index 912692d..a249c32 100644 --- a/.clang-format +++ b/.clang-format @@ -1,4 +1,59 @@ --- -IndentWidth: '2' - -... +IndentWidth: 2 +# SPDX-SnippetBegin +# SPDX-License-Identifier: CC0-1.0 +# SPDX-SnippetCopyrightText: NONE +Language: Cpp +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 +# SPDX-SnippetEnd diff --git a/.gitignore b/.gitignore index 48835df..2122d6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /build/ +.vscode *.user 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/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/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 08830e9..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) @@ -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..f4ac15c 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); } @@ -526,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) { @@ -553,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 @@ -603,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 new file mode 100644 index 0000000..f14b121 --- /dev/null +++ b/src/store.c @@ -0,0 +1,309 @@ +// 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 +#include +#include +#include +#include +#include +#include + +#define MAX_NAME_LENGTH 256 + +struct wd_head_config; + +struct profile_line { + int start; + int end; +}; + +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) { + 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); + } + } else { // configDir is $XDG_CONFIG_HOME + snprintf(defaultConfigDir, sizeof(defaultConfigDir), "%s", configDir); + } + } + + // set default kanshi config path + snprintf(kanshiConfigPath, sizeof(kanshiConfigPath), "%s/kanshi/config", defaultConfigDir); + + // 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); + } + + // 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, const 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) { + 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; + 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 + 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 + 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; + if (profileMatchedNum == num) ps = Found; + } else { + char outputName[MAX_NAME_LENGTH]; + // 从当前行提取输出名称 + char *trimmedBuffer = buffer; + while (isspace(*trimmedBuffer)) { + trimmedBuffer++; // skip leading spaces + } + 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; + 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; + ps = Looking_for_profile; + } + } + break; + } + } + fclose(configFile); + 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 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); + + char *descriptions[HEADS_MAX]; + for (int i = 0; i < HEADS_MAX; i++) descriptions[i] = NULL; + + 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; + wl_list_for_each(output, outputs, link) { + struct wd_head *head = output->head; + + // for transform + char *trans_str; + switch (output->transform) { + 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"; + }; + +<<<<<<< HEAD + if (description_index < MAX_MONITORS_NUM) { + 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->name, output->x, output->y, output->width, + output->height, output->refresh / 1.0e3, output->scale, trans_str); +======= + 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); +>>>>>>> master + description_index++; + } else { + dprintf(2, "Too many monitor!\n\t%i is the maximum allowed number", HEADS_MAX); + return 1; + } + } + + 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) { + 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 < num_of_monitors; i++) { + fprintf(file, " %s\n", outputConfigs[i]); + free(outputConfigs[i]); + } + fprintf(file, "}"); + fclose(file); + } else if (matched_profile.start < matched_profile.end) { + // rewrite corresponding lines + FILE *file = fopen(file_name, "r"); + if (file == NULL) { + perror("File open failed."); + return 1; + } + FILE *tmp = fopen(tmp_file_name, "w"); + if (tmp == NULL) { + dprintf(2, "%s:%i:%s(): Can't create %s : ", __FILE__, __LINE__, __func__, tmp_file_name); + perror(NULL); + fclose(file); + return 1; + } + char _buffer[LINE_MAX]; + int _line = 0; + int _i_output = 0; + while (fgets(_buffer, sizeof(_buffer), file) != NULL) { + if (_line >= 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; +} diff --git a/src/wdisplays.h b/src/wdisplays.h index 4824017..6ed7605 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,22 @@ 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 + */ +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); +// SPDX-SnippetEnd #endif