suckless/clipmenu/src/config.c

500 lines
15 KiB
C

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#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();
}
}
}