diff --git a/README.md b/README.md index b660d15..d9e2e13 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,20 @@ Run: swindings ``` +### Options + + +| Flag | Description | +|------|-------------| +| `-i`, `--follow-includes` | Resolve `include` directives in the sway config file. Glob patterns are supported (e.g. `include conf.d/*.conf`). Disabled by default. | + + +**Example** — read keybindings from a config that uses `include`: + +```bash +swindings --follow-includes +``` + ## Theming diff --git a/src/cli.c b/src/cli.c index e46529d..003d79e 100644 --- a/src/cli.c +++ b/src/cli.c @@ -27,13 +27,19 @@ static struct cag_option options[] = { .access_name = "sway-config", .value_name = "FILE", .description = "Path to sway config. Defaults to None. When None, sway " - "path resolution is followed."}}; + "path resolution is followed."}, + {.identifier = 'i', + .access_letters = "i", + .access_name = "follow-includes", + .value_name = NULL, + .description = "Resolve 'include' directives in the sway config file."}}; static void init_args(cli_args *args) { args->help = false; args->version = false; args->config = NULL; args->sway_config = NULL; + args->follow_includes = false; } bool parse_cli(int argc, char **argv, cli_args *args) { @@ -60,6 +66,9 @@ bool parse_cli(int argc, char **argv, cli_args *args) { case 's': args->sway_config = (char *)cag_option_get_value(&context); break; + case 'i': + args->follow_includes = true; + break; case '?': cag_option_print_error(&context, stderr); return false; diff --git a/src/cli.h b/src/cli.h index 29b4564..cd066a1 100644 --- a/src/cli.h +++ b/src/cli.h @@ -12,6 +12,7 @@ typedef struct { bool version; char *config; char *sway_config; + bool follow_includes; } cli_args; /** diff --git a/src/config.c b/src/config.c index 5ee8238..1479dbf 100644 --- a/src/config.c +++ b/src/config.c @@ -3,6 +3,8 @@ #include "structures.h" #include "utils.h" #include +#include +#include #include #include #include @@ -21,7 +23,8 @@ static void capitalize_into(const char *src, char *buf) { buf[0] = (char)toupper((unsigned char)buf[0]); } -config_error_t config_read_file(const char *filepath, stringlist_t *out) { +config_error_t config_read_file(const char *filepath, stringlist_t *out, + bool follow_includes) { if (filepath == NULL || out == NULL) { return CONFIG_ERR_INVALID_ARGUMENT; } @@ -43,6 +46,41 @@ config_error_t config_read_file(const char *filepath, stringlist_t *out) { if (*pos == '\0' || *pos == '#') continue; + if (follow_includes && strncmp(pos, "include ", 8) == 0) { + char *pattern = pos + 8; + while (*pattern == ' ' || *pattern == '\t') + pattern++; + glob_t globbuf; + char *expanded = NULL; + if (pattern[0] == '~') { + const char *home = getenv("HOME"); + if (!home) { + return CONFIG_ERR_ENV_FAILED; + } + if (asprintf(&expanded, "%s%s", home, pattern + 1) < 0) { + return CONFIG_ERR_ALLOC_FAILED; + } + pattern = expanded; + } + int ret = glob(pattern, GLOB_NOCHECK, NULL, &globbuf); + free(expanded); + if (ret != 0 && ret != GLOB_NOMATCH) { + globfree(&globbuf); + free(line); + int err = fclose(fp); + if (err) + return CONFIG_ERR_IO; + + return CONFIG_ERR_GLOB_FAILED; + } + for (size_t gi = 0; gi < globbuf.gl_pathc; gi++) { + /* Silently skip missing files from glob expansions */ + config_read_file(globbuf.gl_pathv[gi], out, follow_includes); + } + globfree(&globbuf); + continue; + } + if (strncmp(pos, PATTERN, sizeof(PATTERN) - 1) == 0 && pos[sizeof(PATTERN) - 1] == ' ') { if (stringlist_append(out, pos) != 0) { @@ -87,7 +125,7 @@ char *config_get_sway_filepath(void) { return NULL; config_home = config_home_fallback; } - // NOTE: Copied (with modifications) from + // NOTE: Copied from // https://github.com/swaywm/sway/blob/f1b40bc288f3be3bcc6a3c71f28ca9bb2529e70b/sway/config.c struct config_path { const char *prefix; @@ -99,9 +137,7 @@ char *config_get_sway_filepath(void) { {.prefix = config_home, .config_folder = "sway"}, {.prefix = home, .config_folder = ".i3"}, {.prefix = config_home, .config_folder = "i3"}, - // NOTE: Different from original {.prefix = SYSCONFDIR, .config_folder = "sway"}, - // NOTE: Different from original {.prefix = SYSCONFDIR, .config_folder = "i3"}}; size_t num_config_paths = sizeof(config_paths) / sizeof(config_paths[0]); diff --git a/src/config.h b/src/config.h index ee36c83..6748698 100644 --- a/src/config.h +++ b/src/config.h @@ -7,6 +7,7 @@ #endif +#include #include #include "structures.h" @@ -22,6 +23,8 @@ typedef enum { CONFIG_ERR_INVALID_ARGUMENT, CONFIG_ERR_ALLOC_FAILED, CONFIG_ERR_IO, + CONFIG_ERR_GLOB_FAILED, + CONFIG_ERR_ENV_FAILED, } config_error_t; typedef struct { @@ -56,7 +59,7 @@ void keymaplist_free(KeyMapList *list); void keymap_free(KeyMap *km); -config_error_t config_read_file(const char *filepath, stringlist_t *out); +config_error_t config_read_file(const char *filepath, stringlist_t *out, bool follow_includes); // Returns the path to the sway config file, or NULL on error. // Caller must free() the returned string. diff --git a/src/main.c b/src/main.c index 7486b3e..d38215d 100644 --- a/src/main.c +++ b/src/main.c @@ -52,7 +52,7 @@ int main(int argc, char *argv[]) { return ConfigError; } - if (config_read_file(filepath, &list) != 0) { + if (config_read_file(filepath, &list, args.follow_includes) != 0) { if (fprintf(stderr, "failed to read file\n")) return IOError; free(filepath); diff --git a/tests/all_test.c b/tests/all_test.c index 54793ff..de69b23 100644 --- a/tests/all_test.c +++ b/tests/all_test.c @@ -56,6 +56,10 @@ extern void test_sway_filepath_falls_back_to_i3(void); extern void test_sway_filepath_none_exist_returns_null(void); extern void test_sway_filepath_no_home_no_xdg_returns_null(void); extern void test_sway_filepath_xdg_config_home_i3(void); +extern void test_include_ignored_when_follow_includes_false(void); +extern void test_include_resolved_when_follow_includes_true(void); +extern void test_include_glob_resolves_multiple_files(void); +extern void test_include_missing_file_is_silently_skipped(void); int main(void) { UNITY_BEGIN(); @@ -91,6 +95,10 @@ int main(void) { RUN_TEST(test_sway_filepath_none_exist_returns_null); RUN_TEST(test_sway_filepath_no_home_no_xdg_returns_null); RUN_TEST(test_sway_filepath_xdg_config_home_i3); + RUN_TEST(test_include_ignored_when_follow_includes_false); + RUN_TEST(test_include_resolved_when_follow_includes_true); + RUN_TEST(test_include_glob_resolves_multiple_files); + RUN_TEST(test_include_missing_file_is_silently_skipped); return UNITY_END(); } diff --git a/tests/config_test.c b/tests/config_test.c index 3fb4f00..078566c 100644 --- a/tests/config_test.c +++ b/tests/config_test.c @@ -1,6 +1,7 @@ #define _XOPEN_SOURCE 700 #include "config.h" #include "ftw.h" +#include "structures.h" #include "unity.h" #include #include @@ -9,6 +10,12 @@ #include #include +#define CHECKED_SNPRINTF(buf, fmt, ...) \ + do { \ + if (snprintf(buf, sizeof(buf), fmt, __VA_ARGS__) < 0) \ + perror("snprintf failed"); \ + } while (0) + static char *make_tmpdir(void) { char tmpl[] = "/tmp/config_test_XXXXXX"; char *p = mkdtemp(tmpl); @@ -20,8 +27,7 @@ static char *make_tmpdir(void) { static void mkdir_p(const char *dir) { char buf[PATH_MAX]; - if (snprintf(buf, sizeof(buf), "%s", dir) < 0) - return; + CHECKED_SNPRINTF(buf, "%s", dir); for (char *p = buf + 1; *p; p++) { if (*p == '/') { *p = '\0'; @@ -48,14 +54,22 @@ int remove_recursively(const char *path) { return nftw(path, remove_dir, 64, FTW_DEPTH | FTW_PHYS); } +static void write_file(const char *path, const char *content) { + FILE *f = fopen(path, "w"); + if (!f) + return; + if (fputs(content, f) != 0) + perror("write failed"); + if (fclose(f) != 0) + perror("write failed"); +} + void test_sway_filepath_home_sway_dir(void) { char *base = make_tmpdir(); char dir[PATH_MAX], path[PATH_MAX]; - if (snprintf(dir, sizeof(dir), "%s/.sway", base) < 0) - perror("snprintf failed"); - if (snprintf(path, sizeof(path), "%s/config", dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(dir, "%s/.sway", base); + CHECKED_SNPRINTF(path, "%s/config", dir); mkdir_p(dir); touch(path); @@ -75,10 +89,8 @@ void test_sway_filepath_xdg_config_home_sway(void) { char *base = make_tmpdir(); char dir[PATH_MAX], path[PATH_MAX]; - if (snprintf(dir, sizeof(dir), "%s/sway", base) < 0) - perror("snprintf failed"); - if (snprintf(path, sizeof(path), "%s/config", dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(dir, "%s/sway", base); + CHECKED_SNPRINTF(path, "%s/config", dir); mkdir_p(dir); touch(path); @@ -102,18 +114,14 @@ void test_sway_filepath_home_sway_beats_xdg(void) { char *xdg = make_tmpdir(); char home_dir[PATH_MAX], home_path[PATH_MAX]; - if (snprintf(home_dir, sizeof(home_dir), "%s/.sway", home) < 0) - perror("snprintf failed"); - if (snprintf(home_path, sizeof(home_path), "%s/config", home_dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(home_dir, "%s/.sway", home); + CHECKED_SNPRINTF(home_path, "%s/config", home_dir); mkdir_p(home_dir); touch(home_path); char xdg_dir[PATH_MAX], xdg_path[PATH_MAX]; - if (snprintf(xdg_dir, sizeof(xdg_dir), "%s/sway", xdg) < 0) - perror("snprintf failed"); - if (snprintf(xdg_path, sizeof(xdg_path), "%s/config", xdg_dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(xdg_dir, "%s/sway", xdg); + CHECKED_SNPRINTF(xdg_path, "%s/config", xdg_dir); mkdir_p(xdg_dir); touch(xdg_path); @@ -135,10 +143,8 @@ void test_sway_filepath_falls_back_to_i3(void) { char *home = make_tmpdir(); char dir[PATH_MAX], path[PATH_MAX]; - if (snprintf(dir, sizeof(dir), "%s/.i3", home) < 0) - perror("snprinf failed"); - if (snprintf(path, sizeof(path), "%s/config", dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(dir, "%s/.i3", home); + CHECKED_SNPRINTF(path, "%s/config", dir); mkdir_p(dir); touch(path); @@ -180,10 +186,8 @@ void test_sway_filepath_xdg_config_home_i3(void) { char *xdg = make_tmpdir(); char dir[PATH_MAX], path[PATH_MAX]; - if (snprintf(dir, sizeof(dir), "%s/i3", xdg) < 0) - perror("snprintf failed"); - if (snprintf(path, sizeof(path), "%s/config", dir) < 0) - perror("snprintf failed"); + CHECKED_SNPRINTF(dir, "%s/i3", xdg); + CHECKED_SNPRINTF(path, "%s/config", dir); mkdir_p(dir); touch(path); @@ -200,3 +204,106 @@ void test_sway_filepath_xdg_config_home_i3(void) { free(home); free(xdg); } + +void test_include_ignored_when_follow_includes_false(void) { + char *base = make_tmpdir(); + + char inc_path[PATH_MAX]; + CHECKED_SNPRINTF(inc_path, "%s/extra.conf", base); + write_file(inc_path, "bindsym $mod+x exec xterm # Extra terminal\n"); + + char main_path[PATH_MAX]; + CHECKED_SNPRINTF(main_path, "%s/config", base); + char main_content[PATH_MAX + 64]; + CHECKED_SNPRINTF(main_content, + "bindsym $mod+Return exec foot # Terminal\ninclude %s\n", + inc_path); + write_file(main_path, main_content); + + stringlist_t list; + stringlist_init(&list); + config_error_t err = config_read_file(main_path, &list, false); + TEST_ASSERT_EQUAL_INT(CONFIG_SUCCESS, err); + TEST_ASSERT_EQUAL_size_t(1, list.count); + TEST_ASSERT_TRUE(strncmp(list.items[0], "bindsym $mod+Return", 19) == 0); + + stringlist_free(&list); + remove_recursively(base); + free(base); +} + +void test_include_resolved_when_follow_includes_true(void) { + char *base = make_tmpdir(); + + char inc_path[PATH_MAX]; + CHECKED_SNPRINTF(inc_path, "%s/extra.conf", base); + write_file(inc_path, "bindsym $mod+x exec xterm # Extra terminal\n"); + + char main_path[PATH_MAX]; + CHECKED_SNPRINTF(main_path, "%s/config", base); + char main_content[PATH_MAX + 64]; + CHECKED_SNPRINTF(main_content, + "bindsym $mod+Return exec foot # Terminal\ninclude %s\n", + inc_path); + write_file(main_path, main_content); + + stringlist_t list; + stringlist_init(&list); + config_error_t err = config_read_file(main_path, &list, true); + TEST_ASSERT_EQUAL_INT(CONFIG_SUCCESS, err); + TEST_ASSERT_EQUAL_size_t(2, list.count); + + stringlist_free(&list); + remove_recursively(base); + free(base); +} + +void test_include_glob_resolves_multiple_files(void) { + char *base = make_tmpdir(); + + char conf_dir[PATH_MAX]; + CHECKED_SNPRINTF(conf_dir, "%s/conf.d", base); + mkdir_p(conf_dir); + + char f1[PATH_MAX], f2[PATH_MAX]; + CHECKED_SNPRINTF(f1, "%s/01.conf", conf_dir); + CHECKED_SNPRINTF(f2, "%s/02.conf", conf_dir); + write_file(f1, "bindsym $mod+1 workspace 1 # WS 1\n"); + write_file(f2, "bindsym $mod+2 workspace 2 # WS 2\n"); + + char main_path[PATH_MAX]; + CHECKED_SNPRINTF(main_path, "%s/config", base); + char main_content[PATH_MAX + 64]; + CHECKED_SNPRINTF(main_content, "include %s/*.conf\n", conf_dir); + write_file(main_path, main_content); + + stringlist_t list; + stringlist_init(&list); + config_error_t err = config_read_file(main_path, &list, true); + TEST_ASSERT_EQUAL_INT(CONFIG_SUCCESS, err); + TEST_ASSERT_EQUAL_size_t(2, list.count); + + stringlist_free(&list); + remove_recursively(base); + free(base); +} + +void test_include_missing_file_is_silently_skipped(void) { + char *base = make_tmpdir(); + + char main_path[PATH_MAX]; + CHECKED_SNPRINTF(main_path, "%s/config", base); + write_file(main_path, + "bindsym $mod+Return exec foot # Terminal\n" + "include /nonexistent/path/that/does/not/exist.conf\n"); + + stringlist_t list; + stringlist_init(&list); + config_error_t err = config_read_file(main_path, &list, true); + TEST_ASSERT_EQUAL_INT(CONFIG_SUCCESS, err); + TEST_ASSERT_EQUAL_size_t(1, list.count); + + stringlist_free(&list); + remove_recursively(base); + free(base); +}