#include #include #include #include #include #include #include #include #include "config.h" #include "x.h" /** * Determines the runtime directory for storing application data. This is _not_ * the clip store, but the place to create the directory that will become it * (unless CM_DIR is set, in which case we use that directly). */ static const char *get_runtime_directory(void) { static const char *runtime_dir = NULL; const char *env_vars[] = {"XDG_RUNTIME_DIR", "TMPDIR"}; if (runtime_dir) { return runtime_dir; } for (size_t i = 0; i < arrlen(env_vars); i++) { const char *dir = getenv(env_vars[i]); if (dir) { return (runtime_dir = dir); } } return (runtime_dir = "/tmp"); } /** * Constructs the path to the clip store directory for clipmenu, creating the * directory if it does not exist. */ char *get_cache_dir(struct config *cfg) { expect(cfg->ready); static char cache_dir[PATH_MAX]; // In case config changed, do the write anyway snprintf_safe(cache_dir, PATH_MAX, "%s/clipmenu.%d.%ld", cfg->runtime_dir, CLIPMENU_VERSION, (long)getuid()); die_on(mkdir(cache_dir, S_IRWXU) != 0 && errno != EEXIST, "Failed to create directory: %s\n", cache_dir); return cache_dir; } static bool is_absolute_path(const char *path) { return path && path[0] == '/'; } static int resolve_path(const char *input, void *output) { char **out_path = output; if (!input) { return -EINVAL; } if (input[0] == '~') { if (input[1] != '\0' && input[1] != '/') { return -EINVAL; } const char *home = getenv("HOME"); if (!home) { return -EINVAL; } size_t len = strlen(home) + strlen(input); char *expanded = malloc(len); expect(expanded); snprintf_safe(expanded, len, "%s%s", home, input + 1); *out_path = expanded; } else { char *dup = strdup(input); expect(dup); *out_path = dup; } if (!is_absolute_path(*out_path)) { free(*out_path); *out_path = NULL; return -EINVAL; } return 0; } /** * This whole section consists of conversion functions to go from a string in * the config file to the type we expect for `struct Config`. */ static int convert_bool(const char *str, void *output) { const char *const truthy[] = {"1", "y", "yes", "true", "on"}; const char *const falsy[] = {"0", "n", "no", "false", "off"}; for (size_t i = 0; i < arrlen(truthy); i++) { if (strceq(str, truthy[i])) { *(bool *)output = true; return 0; } } for (size_t i = 0; i < arrlen(falsy); i++) { if (strceq(str, falsy[i])) { *(bool *)output = false; return 0; } } return -EINVAL; } static int convert_positive_int(const char *str, void *output) { char *end; long val = strtol(str, &end, 10); if (*end != '\0' || end == str || val < 0 || val > INT_MAX) { return -EINVAL; } *(int *)output = (int)val; return 0; } static int convert_ignore_window(const char *str, void *output) { struct ignore_window *iw = output; iw->set = (bool)str; if (!iw->set) { return 0; } if (regcomp(&iw->rgx, str, REG_EXTENDED | REG_NOSUB)) { return -EINVAL; } return 0; } static int convert_cm_dir(const char *str, void *output) { if (!str) { str = get_runtime_directory(); } return resolve_path(str, output); } static int _nonnull_ convert_launcher(const char *str, void *output) { struct launcher *lnch = output; lnch->custom = strdup(str); expect(lnch->custom); if (streq(str, "rofi")) { lnch->ltype = LAUNCHER_ROFI; } else { lnch->ltype = LAUNCHER_CUSTOM; } return 0; } #define DEFAULT_SELECTION_STATE(name) \ (struct selection) { name, 0, NULL } static char *skip_whitespace(char *str) { while (*str != '\0' && isspace((unsigned char)*str)) { str++; } return str; } static int convert_selections(const char *str, void *output) { struct selection *sels = malloc(3 * sizeof(struct selection)); expect(sels); sels[CM_SEL_CLIPBOARD] = DEFAULT_SELECTION_STATE("clipboard"); sels[CM_SEL_PRIMARY] = DEFAULT_SELECTION_STATE("primary"); sels[CM_SEL_SECONDARY] = DEFAULT_SELECTION_STATE("secondary"); _drop_(free) char *inner_str = strdup(str); expect(inner_str); char *next = skip_whitespace(inner_str); while (*next != '\0') { const char *token = next; while (*next != '\0' && !isspace((unsigned char)*next)) { next++; } if (*next != '\0') { *next++ = '\0'; next = skip_whitespace(next); } bool found = false; for (size_t i = 0; i < CM_SEL_MAX; i++) { if (streq(token, sels[i].name)) { sels[i].active = true; found = true; break; } } if (!found) { return -EINVAL; } } *(struct selection **)output = sels; return 0; } /** * Constructs the path to the clipmenu configuration file. The user can * manually specify a path with $CM_CONFIG, otherwise, it's inferred based on * $XDG_CONFIG_HOME or ~/.config. It's typically * ~/.config/clipmenu/clipmenu.conf. */ static int get_config_file(char *config_path, size_t config_path_len) { const char *cm_config = getenv("CM_CONFIG"); const char *xdg_config_home = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); _drop_(free) char *resolved = NULL; if (cm_config) { int ret = resolve_path(cm_config, &resolved); if (ret != 0) { fprintf(stderr, "Invalid config path from $CM_CONFIG: expected absolute " "path or leading ~\n"); return ret; } snprintf_safe(config_path, config_path_len, "%s", resolved); } else if (xdg_config_home) { int ret = resolve_path(xdg_config_home, &resolved); if (ret != 0) { fprintf(stderr, "Invalid config path from $XDG_CONFIG_HOME: expected " "absolute path or leading ~\n"); return ret; } snprintf_safe(config_path, config_path_len, "%s/clipmenu/clipmenu.conf", resolved); } else { die_on(!home, "None of $CM_CONFIG, $XDG_CONFIG_HOME, or $HOME is set\n"); int ret = resolve_path(home, &resolved); if (ret != 0) { fprintf(stderr, "Invalid config path from $HOME: expected absolute path or " "leading ~\n"); return ret; } snprintf_safe(config_path, config_path_len, "%s/.config/clipmenu/clipmenu.conf", resolved); } return 0; } static int config_parse_env_vars(struct config_entry entries[], size_t entries_len) { for (size_t i = 0; i < entries_len; ++i) { const char *env_var = entries[i].env_var; if (!env_var) { continue; } const char *env_value = getenv(env_var); if (env_value && entries[i].convert(env_value, entries[i].value) != 0) { fprintf(stderr, "Error parsing environment variable for $%s\n", env_var); return -EINVAL; } else if (env_value) { dbg("Config entry %s is set to %s by $%s\n", entries[i].config_key, env_value, env_var); entries[i].is_set = true; } } return 0; } static int config_parse_file(FILE *file, struct config_entry entries[], size_t entries_len) { if (!file) { return 0; } char line[256]; while (fgets(line, sizeof(line), file)) { char *key = skip_whitespace(line); if (*key == '\0' || *key == '#') { continue; } char *value = key; while (*value != '\0' && !isspace((unsigned char)*value)) { value++; } if (*value == '\0') { continue; } *value++ = '\0'; value = skip_whitespace(value); char *newline = strpbrk(value, "\r\n"); if (newline) { *newline = '\0'; } for (size_t i = 0; i < entries_len; ++i) { if (!entries[i].is_set && streq(entries[i].config_key, key)) { if (entries[i].convert(value, entries[i].value) != 0) { fprintf(stderr, "Error parsing config file for %s\n", entries[i].config_key); return -EINVAL; } dbg("Config entry %s is set to %s by config file\n", entries[i].config_key, value); entries[i].is_set = true; break; } } } return 0; } static int config_apply_default_values(struct config_entry entries[], size_t entries_len) { for (size_t i = 0; i < entries_len; ++i) { if (!entries[i].is_set) { if (entries[i].convert(entries[i].default_value, entries[i].value) != 0) { fprintf(stderr, "Error setting default value for %s\n", entries[i].config_key); return -EINVAL; } dbg("Config entry %s is set to %s by fallback\n", entries[i].config_key, entries[i].default_value); } } return 0; } /** * Parse the clipmenu configuration file and environment variables to set up * the application configuration. * * Prior to version 7, clipmenu and friends could only be configured via * environment variables, so these are supported for backwards compatibility. * In general, it's more straightforward to use the config file nowadays. * * This is generally not expected to be called by applications -- call * config_setup() instead, which provides the right file for you. */ static int config_setup_internal(FILE *file, struct config *cfg) { struct config_entry entries[] = { {"max_clips", "CM_MAX_CLIPS", &cfg->max_clips, convert_positive_int, "1000", 0}, {"max_clips_batch", "CM_MAX_CLIPS_BATCH", &cfg->max_clips_batch, convert_positive_int, "100", 0}, {"oneshot", "CM_ONESHOT", &cfg->oneshot, convert_positive_int, "0", 0}, {"deduplicate", "CM_DEDUPLICATE", &cfg->deduplicate, convert_bool, "0", 0}, {"own_clipboard", "CM_OWN_CLIPBOARD", &cfg->own_clipboard, convert_bool, "0", 0}, {"selections", "CM_SELECTIONS", &cfg->selections, convert_selections, "clipboard primary", 0}, {"own_selections", "CM_OWN_SELECTIONS", &cfg->owned_selections, convert_selections, "clipboard", 0}, {"ignore_window", "CM_IGNORE_WINDOW", &cfg->ignore_window, convert_ignore_window, NULL, 0}, {"launcher", "CM_LAUNCHER", &cfg->launcher, convert_launcher, "dmenu", 0}, {"launcher_pass_dmenu_args", "CM_LAUNCHER_PASS_DMENU_ARGS", &cfg->launcher_pass_dmenu_args, convert_bool, "1", 0}, {"touch_on_select", NULL, &cfg->touch_on_select, convert_bool, "0", 0}, {"partial_merge_secs", NULL, &cfg->partial_merge_secs, convert_positive_int, "2", 0}, {"cm_dir", "CM_DIR", &cfg->runtime_dir, convert_cm_dir, NULL, 0}}; size_t entries_len = arrlen(entries); int ret = config_parse_env_vars(entries, entries_len); if (ret < 0) { return ret; } ret = config_parse_file(file, entries, entries_len); if (ret < 0) { return ret; } ret = config_apply_default_values(entries, entries_len); if (ret < 0) { return ret; } cfg->ready = true; return 0; } /** * Frees dynamically allocated memory within the config structure. */ void config_free(struct config *cfg) { free(cfg->runtime_dir); free(cfg->launcher.custom); free(cfg->selections); free(cfg->owned_selections); if (cfg->ignore_window.set) { regfree(&cfg->ignore_window.rgx); } } /** * Initialise the clipmenu configuration by loading settings from environment * variables and the configuration file. */ static void config_setup(struct config *cfg) { char config_path[PATH_MAX]; die_on(get_config_file(config_path, sizeof(config_path)) != 0, "Invalid config\n"); _drop_(fclose) FILE *file = fopen(config_path, "r"); expect(file || errno == ENOENT); die_on(config_setup_internal(file, cfg) != 0, "Invalid config\n"); } static char stdout_buf[512]; const char *prog_name = "broken"; /** * Performs initial setup for clipmenu applications, including setting the * program name (used for dbg()), making sure stdout is line buffered, getting * the config, and setting up X11 error handling. */ struct config setup(const char *inner_prog_name) { struct config cfg; prog_name = inner_prog_name; expect(setvbuf(stdout, stdout_buf, _IOLBF, sizeof(stdout_buf)) == 0); config_setup(&cfg); XSetErrorHandler(xerror_handler); return cfg; } void setup_selections(Display *dpy, struct cm_selections *sels) { sels[CM_SEL_CLIPBOARD].selection = XInternAtom(dpy, "CLIPBOARD", False); sels[CM_SEL_CLIPBOARD].storage = XInternAtom(dpy, "CLIPMENUD_CUR_CLIPBOARD", False); sels[CM_SEL_PRIMARY].selection = XA_PRIMARY; sels[CM_SEL_PRIMARY].storage = XInternAtom(dpy, "CLIPMENUD_CUR_PRIMARY", False); sels[CM_SEL_SECONDARY].selection = XA_SECONDARY; sels[CM_SEL_SECONDARY].storage = XInternAtom(dpy, "CLIPMENUD_CUR_SECONDARY", False); } /** * Maps an Atom to a selection_type based on the selection atoms in the * provided cm_selections struct. Returns CM_SEL_INVALID on error if the * selection type is not found. */ enum selection_type selection_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { for (size_t i = 0; i < CM_SEL_MAX; ++i) { if (sels[i].selection == atom) { return i; } } return CM_SEL_INVALID; } /** * Maps an Atom to a selection_type based on the storage atoms in the provided * cm_selections struct. Returns CM_SEL_INVALID on error if the storage type is * not found. */ enum selection_type storage_atom_to_selection_type(Atom atom, const struct cm_selections *sels) { for (size_t i = 0; i < CM_SEL_MAX; ++i) { if (sels[i].storage == atom) { return i; } } return CM_SEL_INVALID; } void exec_man(void) { execlp("man", "man", prog_name, NULL); die("Failed to exec man: %s\n", strerror(errno)); } // cppcheck doesn't know this comes from an outside type // cppcheck-suppress [constParameter,unmatchedSuppression] void exec_man_on_help(int argc, char *argv[]) { for (int i = 1; i < argc; i++) { if (streq(argv[i], "--")) { break; } if (streq(argv[i], "-h") || streq(argv[i], "--help")) { exec_man(); } } }