#include #include #include #include #include #include #include "any_log.h" #include "config.h" #include "util.h" #include "block.h" #include "action.h" #define ANY_INI_IMPLEMENT #include "any_ini.h" const config_entry_t bar_entries[] = { { "override-redirect", CONFIG_BOOL, NULL, offsetof(config_t, override_redirect) }, { "action-strict-run", CONFIG_BOOL, NULL, offsetof(config_t, action_strict_run) }, { "action-strict-set", CONFIG_BOOL, NULL, offsetof(config_t, action_strict_set) }, { "action-fail-fast", CONFIG_BOOL, NULL, offsetof(config_t, action_failfast) }, { "wm-name", CONFIG_STRING, NULL, offsetof(config_t, wm_name) }, { "wm-struts", CONFIG_BOOL, NULL, offsetof(config_t, wm_struts) }, { "background", CONFIG_GRADIENT, NULL, offsetof(config_t, background) }, { "font", CONFIG_STRING, NULL, offsetof(config_t, font) }, { "monitor", CONFIG_STRING, NULL, offsetof(config_t, monitor) }, { "width", CONFIG_UINT, NULL, offsetof(config_t, width) }, { "height", CONFIG_UINT, NULL, offsetof(config_t, height) }, { "x-offset", CONFIG_UINT, NULL, offsetof(config_t, x_offset) }, { "y-offset", CONFIG_UINT, NULL, offsetof(config_t, y_offset) }, { "scale", CONFIG_DOUBLE, NULL, offsetof(config_t, scale) }, { 0 }, }; const config_enum_t text_align_enum[] = { { "left", ALIGN_LEFT }, { "center", ALIGN_CENTER }, { "right", ALIGN_RIGHT }, { 0 }, }; const config_entry_t block_entries[] = { { "hidden", CONFIG_BOOL, NULL, offsetof(block_t, hidden) }, { "color", CONFIG_GRADIENT, NULL, offsetof(block_t, bg_color) }, { "line-color", CONFIG_GRADIENT, NULL, offsetof(block_t, line_color) }, { "line-width", CONFIG_UINT, NULL, offsetof(block_t, line_width) }, { "x-padding", CONFIG_UINT, NULL, offsetof(block_t, x_padding) }, { "y-padding", CONFIG_UINT, NULL, offsetof(block_t, y_padding) }, { "min-width", CONFIG_UINT, NULL, offsetof(block_t, min_width) }, { "max-width", CONFIG_UINT, NULL, offsetof(block_t, max_width) }, { "interval", CONFIG_TIME, NULL, offsetof(block_t, update_interval) }, { "trigger", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_TRIGGER]) }, { "left-click", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_LEFT_CLICK]) }, { "middle-click", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_MIDDLE_CLICK]) }, { "right-click", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_RIGHT_CLICK]) }, { "scroll-up", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_SCROLL_UP]) }, { "scroll-down", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_SCROLL_DOWN]) }, { "hover-start", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_HOVER_START]) }, { "hover-move", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_HOVER_MOVE]) }, { "hover-stop", CONFIG_STRING, NULL, offsetof(block_t, actions[EVENT_HOVER_STOP]) }, { 0 }, }; const config_entry_t block_group_entries[] = { { "spacing", CONFIG_UINT, NULL, offsetof(block_group_t, spacing) }, { "blocks", CONFIG_LIST, NULL, offsetof(block_group_t, children) }, { "collapse", CONFIG_BOOL, NULL, offsetof(block_group_t, collapse) }, { 0 }, }; const config_entry_t block_text_entries[] = { { "text", CONFIG_STRING, NULL, offsetof(block_text_t, text) }, { "text-color", CONFIG_COLOR, NULL, offsetof(block_text_t, text_color) }, { "text-size", CONFIG_UINT, NULL, offsetof(block_text_t, text_size) }, { "text-align", CONFIG_ENUM, text_align_enum, offsetof(block_text_t, text_size) }, { 0 }, }; const char *config_type_to_string(config_type_t type) { static const char *types[] = { "string", "integer", "unsigned integer", "double float", "boolean", "color", "gradient", "enum", "time", "list", }; assert(type >= CONFIG_STRING && type <= CONFIG_LIST); return types[type]; } static bool config_read_string(const char *value, char **result) { if (value == NULL) value = ""; size_t start = 0, end = strlen(value); if (value[0] == '"' && value[end - 1] == '"') { start++; end--; } free(*result); *result = strslice(value, start, end); return true; } static bool config_read_int(const char *value, int *result) { char *end; long n = strtol(value, &end, 0); *result = n; if (n > INT_MAX || n < INT_MIN) log_debug("Integer %ld is out of range", n); return *end == '\0' && n != LONG_MIN && n != LONG_MAX; } static bool config_read_uint(const char *value, unsigned int *result) { char *end; unsigned long n = strtoul(value, &end, 0); *result = n; if (n > UINT_MAX) log_debug("Integer %lu is out of range", n); return *end == '\0' && n != ULONG_MAX; } static bool config_read_double(const char *value, double *result) { char *end; *result = strtod(value, &end); return *end == '\0'; } static bool config_read_bool(const char *value, bool *result) { if (!strcmp(value, "false")) { *result = false; return true; } if (!strcmp(value, "true")) { *result = true; return true; } log_debug("Invalid bool value"); return false; } static bool config_read_color(const char *value, color_t *result) { if (value[0] == '#') { char *end; unsigned long n = strtoul(value + 1, &end, 16); if (end[0] != '\0') return false; int bpc = 0; switch (end - (value + 1)) { case 3: bpc = 4; n = (n << 4) | 0x0f; break; case 6: bpc = 8; n = (n << 8) | 0xff; break; case 4: bpc = 4; break; case 8: bpc = 8; break; default: return false; } const unsigned int single_max = (1 << bpc) - 1; result->r = ((n >> 3 * bpc) & single_max) / (double)single_max; result->g = ((n >> 2 * bpc) & single_max) / (double)single_max; result->b = ((n >> 1 * bpc) & single_max) / (double)single_max; result->a = ((n >> 0 * bpc) & single_max) / (double)single_max; return true; } log_debug("Invalid color '%s'", value); return false; } static bool config_read_gradient(const char *value, gradient_t *result) { gradient_clean(result); memset(result, 0, sizeof(gradient_t)); size_t count = 0; for (size_t i = 0; value[i] != '\0'; ++i) count += value[i] == ','; color_t *colors = calloc(count + 1, sizeof(color_t)); size_t n = 0; char *state, *copy = strcopy(value); for (char *string = copy; ; string = NULL) { char *token = strtok_r(string, ",", &state); if (token == NULL) break; while (isspace(*token)) token++; char *end = token + strlen(token); while (isspace(*end)) end--; *end = '\0'; if (!config_read_color(token, &colors[n++])) { free(copy); free(colors); return false; } } free(copy); if (n == 0) { free(colors); log_debug("Expected at least one color"); return false; } result->colors = colors; result->length = n; if (n == 1) { result->pattern = cairo_pattern_create_rgba(colors->r, colors->g, colors->b, colors->a); return true; } result->pattern = cairo_pattern_create_linear(0, 0, 1, 0); for (size_t i = 0; i < n; i++) { double offset = i * 1.0 / (n - 1); cairo_pattern_add_color_stop_rgba(result->pattern, offset, colors[i].r, colors[i].g, colors[i].b, colors[i].a); } return true; } static bool config_read_enum(const char *value, config_enum_t *data, int *result) { for (size_t i = 0; data[i].label != NULL; i++) { if (!strcmp(value, data[i].label)) { *result = data[i].value; return true; } } log_debug("Invalid enum value '%s'", value); return false; } // TODO: More robust parsing static bool config_read_time(const char *value, struct timespec *result) { unsigned int ms; if (config_read_uint(value, &ms)) { result->tv_sec = ms / 1000; result->tv_nsec = (ms % 1000) * 1000000; return true; } log_debug("Invalid time '%s'", value); return false; } // TODO: Handle quotation marks static bool config_read_list(const char *value, char ***result) { strfreelist(*result); *result = NULL; size_t count = 0; for (size_t i = 0; value[i] != '\0'; ++i) count += value[i] == ','; char **list = calloc(count + 2, sizeof(char *)); size_t n = 0; char *state, *copy = strcopy(value); for (char *string = copy; ; string = NULL) { char *token = strtok_r(string, ",", &state); if (token == NULL) break; while (isspace(*token)) token++; char *end = token + strlen(token); while (isspace(*end)) end--; *end = '\0'; if (!config_read_string(token, &list[n++])) { list[n] = NULL; strfreelist(list); free(copy); return false; } } free(copy); list[n] = NULL; *result = list; return true; } static block_t *config_alloc_block(config_t *config, const block_scheme_t *scheme, char *label) { config->blocks = realloc(config->blocks, ++config->n_blocks * sizeof(block_t *)); assert(config->blocks != NULL); block_t *block = config->blocks[config->n_blocks - 1] = calloc(1, scheme->size); assert(block != NULL); block->label = label; block->scheme = scheme; scheme->init_fn(block); return block; } static config_status_t config_read_block(block_t *block, config_type_t *type, const char *key, const char *value) { size_t index = 0; config_status_t status = config_read_entry(block_entries, block, &index, key, value); *type = block_entries[index].type; if (status != CONFIG_UNKNOWN) return status; if (block->type == BLOCK_GROUP) { status = config_read_entry(block_group_entries, block, &index, key, value); *type = block_group_entries[index].type; } else if (block->type == BLOCK_TEXT) { status = config_read_entry(block_text_entries, block, &index, key, value); *type = block_text_entries[index].type; } if (status != CONFIG_UNKNOWN || block->scheme->entries == NULL) return status; status = config_read_entry(block->scheme->entries, block, &index, key, value); *type = block->scheme->entries[index].type; return status; } static config_status_t config_read_action(action_t *action, const char *key, const char *value) { action_type_t type; if (!strcmp(key, "target")) type = ACTION_TARGET; else if (!strncmp(key, "set-", 4)) type = ACTION_SET_PAIR; else if (!strcmp(key, "run-sync")) type = ACTION_RUN_SYNC; else if (!strcmp(key, "run-async")) type = ACTION_RUN_ASYNC; else return CONFIG_UNKNOWN; action->parts = realloc(action->parts, ++action->length * sizeof(action_part_t)); assert(action->parts != NULL); action->parts[action->length - 1].type = type; action->parts[action->length - 1].key = strcopy(key); action->parts[action->length - 1].value = strcopy(value); return CONFIG_SUCCESS; } // TODO: More robust way to define defaults void config_init(config_t *config) { const config_t config_default = { .font = "monospace 10", .monitor = NULL, .width = 100, .height = 50, .override_redirect = false, }; memcpy(config, &config_default, sizeof(config_t)); config->font = strcopy(config_default.font); extern const block_scheme_t block_group_scheme; block_group_t *bar = (block_group_t *)config_alloc_block(config, &block_group_scheme, strcopy("bar")); bar->spacing = 10; } config_status_t config_read_entry(const config_entry_t *entries, void *result, size_t *index, const char *key, const char *value) { for (size_t i = 0; entries[i].key != NULL; i++) { if (!strcmp(key, entries[i].key)) { if (index != NULL) *index = i; result += entries[i].offset; bool nullable = entries[i].type == CONFIG_STRING || entries[i].type == CONFIG_LIST; if ((value == NULL || *value == '\0') && !nullable) return CONFIG_INVALID; switch (entries[i].type) { case CONFIG_STRING: if (config_read_string(value, (char **)result)) return CONFIG_SUCCESS; break; case CONFIG_INT: if (config_read_int(value, (int *)result)) return CONFIG_SUCCESS; break; case CONFIG_UINT: if (config_read_uint(value, (unsigned int *)result)) return CONFIG_SUCCESS; break; case CONFIG_DOUBLE: if (config_read_double(value, (double *)result)) return CONFIG_SUCCESS; break; case CONFIG_BOOL: if (config_read_bool(value, (bool *)result)) return CONFIG_SUCCESS; break; case CONFIG_COLOR: if (config_read_color(value, (color_t *)result)) return CONFIG_SUCCESS; break; case CONFIG_GRADIENT: if (config_read_gradient(value, (gradient_t *)result)) return CONFIG_SUCCESS; break; case CONFIG_ENUM: if (config_read_enum(value, (config_enum_t *)entries[i].data, (int *)result)) return CONFIG_SUCCESS; break; case CONFIG_TIME: if (config_read_time(value, (struct timespec *)result)) return CONFIG_SUCCESS; break; case CONFIG_LIST: if (config_read_list(value, (char ***)result)) return CONFIG_SUCCESS; break; default: unreachable(); } return CONFIG_INVALID; } } return CONFIG_UNKNOWN; } int config_read(config_t *config, FILE *file) { any_ini_stream_t ini; any_ini_file_init(&ini, file); int n_errors = 0; char *section = NULL; do { char *key = NULL, *value = NULL; int errors = 0; bool bar_section = false; action_t *action = NULL; block_t *block = NULL; if (section != NULL) { if (!strncmp(section, "block.", 6)) { char *label = strcopy(section + 6); if (label == NULL || *label == '\0') { ++errors; log_value_error("Block section must have a non-empty label", "s:section", section, "i:line", ini.line); } else if (!strcmp(label, "bar") || !strcmp(label, "default")) { ++errors; log_value_error("Block section must have a non-reserved label", "s:section", section, "s:label", label, "i:line", ini.line); } else { for (size_t i = 0; i < config->n_blocks; i++) { if (!strcmp(label, config->blocks[i]->label)) { ++errors; log_value_error("Block section must have a unique label", "s:section", section, "s:label", label, "i:line", ini.line); } } } if ((key = any_ini_stream_next_key(&ini)) == NULL || strcmp(key, "type")) { ++errors; log_value_error("Block section must start with a 'type' pair", "s:section", section, "s:wrong_key", key, "i:line", ini.line); goto skip_pair; } value = any_ini_stream_next_value(&ini); if (value == NULL) { ++errors; log_value_error("Block type must be non-empty", "s:section", section, "i:line", ini.line); goto skip_pair; } for (size_t i = 0; block_schemes[i] != NULL; i++) { if (strcmp(block_schemes[i]->name, value)) continue; block = config_alloc_block(config, block_schemes[i], label); goto skip_pair; } ++errors; log_value_error("Block type not supported", "s:section", section, "s:type", value, "i:line", ini.line); goto skip_pair; } else if (!strncmp(section, "action.", 7)) { char *label = strcopy(section + 7); if (label == NULL || *label == '\0') { ++errors; log_value_error("Action section must have a non-empty label", "s:section", section, "i:line", ini.line); } else { for (size_t i = 0; i < config->n_actions; i++) { if (!strcmp(label, config->actions[i].label)) { ++errors; log_value_error("Action section must have a unique label", "s:section", section, "i:line", ini.line); } } } config->actions = realloc(config->actions, ++config->n_actions * sizeof(action_t)); assert(config->actions != NULL); action = &config->actions[config->n_actions - 1]; memset(action, 0, sizeof(action_t)); action->label = label; } else if (!strcmp(section, "bar")) { bar_section = true; } else log_warn("Unknown section '%s'", section); } while ((key = any_ini_stream_next_key(&ini)) != NULL) { value = any_ini_stream_next_value(&ini); log_value_trace("Reading entry", "s:section", section, "s:key", key, "s:value", value, "i:line", ini.line); // Skip unknown sections if (!bar_section && block == NULL && action == NULL) goto skip_pair; config_type_t type; config_status_t status; if (bar_section) { size_t index = 0; status = config_read_entry(bar_entries, config, &index, key, value); // Try the block entries if (status == CONFIG_UNKNOWN) block = config->blocks[0]; else type = bar_entries[index].type; } if (block != NULL) { status = config_read_block(block, &type, key, value); } if (action != NULL) { status = config_read_action(action, key, value); } switch (status) { case CONFIG_SUCCESS: log_value_debug("Parsed entry", "s:section", section, "s:key", key, "s:value", value, "s:type", config_type_to_string(type), "i:line", ini.line); break; case CONFIG_INVALID: errors++; log_value_error("Invalid entry", "s:section", section, "s:key", key, "s:value", value, "s:type", config_type_to_string(type), "i:line", ini.line); break; case CONFIG_UNKNOWN: log_value_warn("Unknown entry", "s:section", section, "s:key", key, "s:value", value, "i:line", ini.line); break; default: unreachable(); } skip_pair: free(key); free(value); } free(section); n_errors += errors; } while ((section = any_ini_stream_next_section(&ini)) != NULL); return n_errors; } int config_validate(config_t *config) { int errors = 0; block_group_t *bar = (block_group_t *)config->blocks[0]; if (bar->children == NULL) { log_error("Section '%s' requires at least one child", "bar"); errors++; } // Validate the config itself if (config->scale < 1 && config->scale != 0) { log_error("Bar '%s' should be at least 1", "scale"); errors++; } // Validate blocks for (size_t i = 0; i < config->n_blocks; i++) { block_t *block = config->blocks[i]; const block_scheme_t *scheme = block->scheme; if (scheme->validate_fn == NULL) continue; log_debug("Validating 'block.%s'", block->label); int tmp = scheme->validate_fn(block, config); errors += tmp; block->validated = tmp == 0; } // Validate actions // for (size_t i = 0; i < config->n_actions; i++) { action_t *action = &config->actions[i]; log_debug("Validating 'action.%s'", action->label); errors += action_validate(action, config); } return errors; } bool config_resolve_action(config_t *config, const char *label, action_t **action) { for (size_t j = 0; j < config->n_actions; j++) { if (!strcmp(label, config->actions[j].label)) { *action = &config->actions[j]; return true; } } return false; } bool config_resolve(config_t *config, block_t **block) { block_group_t *bar = (block_group_t *)config->blocks[0]; assert(bar->children != NULL); for (size_t i = 0; i < config->n_actions; i++) { if (!action_resolve(&config->actions[i], config)) return false; } log_debug("Resolved %zu actions", config->n_actions); for (size_t i = 0; i < config->n_blocks; i++) { if (!block_resolve(config->blocks[i], config)) return false; } log_debug("Resolved %zu blocks", config->n_blocks); *block = config->blocks[0]; bar->block.min_width = bar->block.max_width = config->width; return true; } void config_free(config_t *config) { for (size_t i = 0; i < config->n_blocks; i++) block_free(config->blocks[i]); free(config->blocks); for (size_t i = 0; i < config->n_actions; i++) action_clean(&config->actions[i]); free(config->actions); gradient_clean(&config->background); free(config->font); free(config->monitor); free(config->wm_name); }