diff --git a/config.c b/config.c index 1f5b3efa..94b31b70 100644 --- a/config.c +++ b/config.c @@ -9,11 +9,17 @@ #include #include "config.h" +#include "criteria.h" void init_default_config(struct mako_config *config) { - init_default_style(&config->style); + wl_list_init(&config->criteria); + struct mako_criteria *global_criteria = create_criteria(config); + init_default_style(&global_criteria->style); + + init_empty_style(&config->hidden_style); + config->hidden_style.format = strdup("(%h more)"); + config->hidden_style.spec.format = true; - config->hidden_format = strdup("(%h more)"); config->output = strdup(""); config->max_visible = 5; @@ -26,8 +32,12 @@ void init_default_config(struct mako_config *config) { } void finish_config(struct mako_config *config) { - finish_style(&config->style); - free(config->hidden_format); + struct mako_criteria *criteria, *tmp; + wl_list_for_each_safe(criteria, tmp, &config->criteria, link) { + destroy_criteria(criteria); + } + + finish_style(&config->hidden_style); free(config->output); } @@ -59,11 +69,116 @@ void init_default_style(struct mako_style *style) { memset(&style->spec, true, sizeof(struct mako_style_spec)); } +void init_empty_style(struct mako_style *style) { + memset(style, 0, sizeof(struct mako_style)); +} + void finish_style(struct mako_style *style) { free(style->font); free(style->format); } +// Update `target` with the values specified in `style`. If a failure occurs, +// `target` will remain unchanged. +bool apply_style(struct mako_style *target, const struct mako_style *style) { + // Try to duplicate strings up front in case allocation fails and we have + // to bail without changing `target`. + char *new_font = NULL; + char *new_format = NULL; + + if (style->spec.font) { + new_font = strdup(style->font); + if (new_font == NULL) { + fprintf(stderr, "allocation failed\n"); + return false; + } + } + + if (style->spec.format) { + new_format = strdup(style->format); + if (new_format == NULL) { + fprintf(stderr, "allocation failed\n"); + return false; + } + } + + // Now on to actually setting things! + + if (style->spec.width) { + target->width = style->width; + target->spec.width = true; + } + + if (style->spec.height) { + target->height = style->height; + target->spec.height = true; + } + + if (style->spec.margin) { + target->margin = style->margin; + target->spec.margin = true; + } + + if (style->spec.padding) { + target->padding = style->padding; + target->spec.padding = true; + } + + if (style->spec.border_size) { + target->border_size = style->border_size; + target->spec.border_size = true; + } + + if (style->spec.font) { + free(target->font); + target->font = new_font; + target->spec.font = true; + } + + if (style->spec.markup) { + target->markup = style->markup; + target->spec.markup = true; + } + + if (style->spec.format) { + free(target->format); + target->format = new_format; + target->spec.format = true; + } + + if (style->spec.actions) { + target->actions = style->actions; + target->spec.actions = true; + } + + if (style->spec.default_timeout) { + target->default_timeout = style->default_timeout; + target->spec.default_timeout = true; + } + + if (style->spec.ignore_timeout) { + target->ignore_timeout = style->ignore_timeout; + target->spec.ignore_timeout = true; + } + + if (style->spec.colors.background) { + target->colors.background = style->colors.background; + target->spec.colors.background = true; + } + + if (style->spec.colors.text) { + target->colors.text = style->colors.text; + target->spec.colors.text = true; + } + + if (style->spec.colors.border) { + target->colors.border = style->colors.border; + target->spec.colors.border = true; + } + + return true; +} + static bool parse_int(const char *s, int *out) { errno = 0; char *end; @@ -150,89 +265,74 @@ static bool parse_color(const char *color, uint32_t *out) { return true; } -static bool apply_config_option(struct mako_config *config, const char *section, - const char *name, const char *value) { - // First try to parse this as a global option. - if (section == NULL) { - if (strcmp(name, "max-visible") == 0) { - return parse_int(value, &config->max_visible); - } else if (strcmp(name, "output") == 0) { - free(config->output); - config->output = strdup(value); - return true; - } else if (strcmp(name, "sort") == 0) { - if (strcmp(value, "+priority") == 0) { - config->sort_criteria |= MAKO_SORT_CRITERIA_URGENCY; - config->sort_asc |= MAKO_SORT_CRITERIA_URGENCY; - } else if (strcmp(value, "-priority") == 0) { - config->sort_criteria |= MAKO_SORT_CRITERIA_URGENCY; - config->sort_asc &= ~MAKO_SORT_CRITERIA_URGENCY; - } else if (strcmp(value, "+time") == 0) { - config->sort_criteria |= MAKO_SORT_CRITERIA_TIME; - config->sort_asc |= MAKO_SORT_CRITERIA_TIME; - } else if (strcmp(value, "-time") == 0) { - config->sort_criteria |= MAKO_SORT_CRITERIA_TIME; - config->sort_asc &= ~MAKO_SORT_CRITERIA_TIME; - } - return true; - } else { - // We want to try the style options now, so keep going. - } - } else { - // TODO: criteria support - if (strcmp(section, "hidden") != 0) { - fprintf(stderr, "Only the 'hidden' section is currently supported\n"); - return false; - } - - if (strcmp(name, "format") == 0) { - free(config->hidden_format); - config->hidden_format = strdup(value); - return true; - } else { - fprintf(stderr, "Only 'format' is supported in the 'hidden' section\n"); - return false; +static bool apply_config_option(struct mako_config *config, const char *name, + const char *value) { + if (strcmp(name, "max-visible") == 0) { + return parse_int(value, &config->max_visible); + } else if (strcmp(name, "output") == 0) { + free(config->output); + config->output = strdup(value); + return true; + } else if (strcmp(name, "sort") == 0) { + if (strcmp(value, "+priority") == 0) { + config->sort_criteria |= MAKO_SORT_CRITERIA_URGENCY; + config->sort_asc |= MAKO_SORT_CRITERIA_URGENCY; + } else if (strcmp(value, "-priority") == 0) { + config->sort_criteria |= MAKO_SORT_CRITERIA_URGENCY; + config->sort_asc &= ~MAKO_SORT_CRITERIA_URGENCY; + } else if (strcmp(value, "+time") == 0) { + config->sort_criteria |= MAKO_SORT_CRITERIA_TIME; + config->sort_asc |= MAKO_SORT_CRITERIA_TIME; + } else if (strcmp(value, "-time") == 0) { + config->sort_criteria |= MAKO_SORT_CRITERIA_TIME; + config->sort_asc &= ~MAKO_SORT_CRITERIA_TIME; } + return true; } - // Now try to match on style options. - struct mako_style *style = &config->style; + return false; +} + +static bool apply_style_option(struct mako_style *style, const char *name, + const char *value) { + struct mako_style_spec *spec = &style->spec; if (strcmp(name, "font") == 0) { free(style->font); - style->font = strdup(value); - return true; + return spec->font = !!(style->font = strdup(value)); } else if (strcmp(name, "background-color") == 0) { - return parse_color(value, &style->colors.background); + return spec->colors.background = + parse_color(value, &style->colors.background); } else if (strcmp(name, "text-color") == 0) { - return parse_color(value, &style->colors.text); + return spec->colors.text = parse_color(value, &style->colors.text); } else if (strcmp(name, "width") == 0) { - return parse_int(value, &style->width); + return spec->width = parse_int(value, &style->width); } else if (strcmp(name, "height") == 0) { - return parse_int(value, &style->height); + return spec->height = parse_int(value, &style->height); } else if (strcmp(name, "margin") == 0) { - return parse_directional(value, &style->margin); + return spec->margin = parse_directional(value, &style->margin); } else if (strcmp(name, "padding") == 0) { - return parse_int(value, &style->padding); + return spec->padding = parse_int(value, &style->padding); } else if (strcmp(name, "border-size") == 0) { - return parse_int(value, &style->border_size); + return spec->border_size = parse_int(value, &style->border_size); } else if (strcmp(name, "border-color") == 0) { - return parse_color(value, &style->colors.border); + return spec->colors.border = parse_color(value, &style->colors.border); } else if (strcmp(name, "markup") == 0) { style->markup = strcmp(value, "1") == 0; - return style->markup || strcmp(value, "0") == 0; + return spec->markup = style->markup || strcmp(value, "0") == 0; } else if (strcmp(name, "format") == 0) { free(style->format); - style->format = strdup(value); - return true; + return spec->format = !!(style->format = strdup(value)); } else if (strcmp(name, "default-timeout") == 0) { - return parse_int(value, &style->default_timeout); + return spec->default_timeout = + parse_int(value, &style->default_timeout); } else if (strcmp(name, "ignore-timeout") == 0) { style->ignore_timeout = strcmp(value, "1") == 0; - return style->ignore_timeout || strcmp(value, "0") == 0; - } else { - return false; + return spec->ignore_timeout = ( + style->ignore_timeout || strcmp(value, "0") == 0); } + + return false; } static bool file_exists(const char *path) { @@ -289,34 +389,72 @@ int load_config_file(struct mako_config *config) { int lineno = 0; char *line = NULL; char *section = NULL; + + struct mako_criteria *criteria = global_criteria(config); + size_t n = 0; while (getline(&line, &n, f) > 0) { ++lineno; if (line[0] == '\0' || line[0] == '\n' || line[0] == '#') { continue; } + if (line[strlen(line) - 1] == '\n') { line[strlen(line) - 1] = '\0'; } + if (line[0] == '[' && line[strlen(line) - 1] == ']') { free(section); section = strndup(line + 1, strlen(line) - 2); + if (strcmp(section, "hidden") == 0) { + // Skip making a criteria for the hidden section. + criteria = NULL; + continue; + } + criteria = create_criteria(config); + if (!parse_criteria(section, criteria)) { + fprintf(stderr, "[%s:%d] Invalid criteria definition\n", base, + lineno); + ret = -1; + break; + } continue; } + char *eq = strchr(line, '='); if (!eq) { fprintf(stderr, "[%s:%d] Expected key=value\n", base, lineno); ret = -1; break; } + + bool valid_option = false; eq[0] = '\0'; - if (!apply_config_option(config, section, line, eq + 1)) { + + struct mako_style *target_style; + if (section != NULL && strcmp(section, "hidden") == 0) { + // The hidden criteria is a lie, we store the associated style + // directly on the config because there's no "real" notification + // object to match against it later. + target_style = &config->hidden_style; + } else { + target_style = &criteria->style; + } + + valid_option = apply_style_option(target_style, line, eq + 1); + + if (!valid_option && section == NULL) { + valid_option = apply_config_option(config, line, eq + 1); + } + + if (!valid_option) { fprintf(stderr, "[%s:%d] Failed to parse option '%s'\n", base, lineno, line); ret = -1; break; } } + free(section); free(line); fclose(f); @@ -362,7 +500,8 @@ int parse_config_arguments(struct mako_config *config, int argc, char **argv) { } const char *name = long_options[option_index].name; - if (!apply_config_option(config, NULL, name, optarg)) { + if (!apply_style_option(&global_criteria(config)->style, name, optarg) + || apply_config_option(config, name, optarg)) { fprintf(stderr, "Failed to parse option '%s'\n", name); return -1; } diff --git a/criteria.c b/criteria.c new file mode 100644 index 00000000..4d1ebb61 --- /dev/null +++ b/criteria.c @@ -0,0 +1,300 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include "mako.h" +#include "config.h" +#include "criteria.h" +#include "notification.h" + +struct mako_criteria *create_criteria(struct mako_config *config) { + struct mako_criteria *criteria = calloc(1, sizeof(struct mako_criteria)); + if (criteria == NULL) { + fprintf(stderr, "allocation failed\n"); + return NULL; + } + + wl_list_insert(config->criteria.prev, &criteria->link); + return criteria; +} + +void destroy_criteria(struct mako_criteria *criteria) { + wl_list_remove(&criteria->link); + + finish_style(&criteria->style); + free(criteria->app_name); + free(criteria->app_icon); + free(criteria->category); + free(criteria->desktop_entry); + free(criteria); +} + +bool match_criteria(struct mako_criteria *criteria, + struct mako_notification *notif) { + struct mako_criteria_spec spec = criteria->spec; + + if (spec.app_name && + strcmp(criteria->app_name, notif->app_name) != 0) { + return false; + } + + if (spec.app_icon && + strcmp(criteria->app_icon, notif->app_icon) != 0) { + return false; + } + + if (spec.actionable && + criteria->actionable == wl_list_empty(¬if->actions)) { + return false; + } + + if (spec.urgency && + criteria->urgency != notif->urgency) { + return false; + } + + if (spec.category && + strcmp(criteria->category, notif->category) != 0) { + return false; + } + + if (spec.desktop_entry && + strcmp(criteria->desktop_entry, notif->desktop_entry) != 0) { + return false; + } + + return true; +} + +bool parse_boolean(const char *string, bool *out) { + if (strcasecmp(string, "true") == 0 || strcmp(string, "1") == 0) { + *out = true; + return true; + } else if (strcasecmp(string, "false") == 0 || strcmp(string, "0") == 0) { + *out = false; + return true; + } + + return false; +} + +bool parse_urgency(const char *string, enum mako_notification_urgency *out) { + if (strcasecmp(string, "low") == 0) { + *out = MAKO_NOTIFICATION_URGENCY_LOW; + return true; + } else if (strcasecmp(string, "normal") == 0) { + *out = MAKO_NOTIFICATION_URGENCY_NORMAL; + return true; + } else if (strcasecmp(string, "high") == 0) { + *out = MAKO_NOTIFICATION_URGENCY_HIGH; + return true; + } + + return false; +} + +bool parse_criteria(const char *string, struct mako_criteria *criteria) { + // Create space to build up the current token that we're reading. We know + // that no single token can ever exceed the length of the entire criteria + // string, so that's a safe length to use for the buffer. + int token_max_length = strlen(string) + 1; + char token[token_max_length]; + memset(token, 0, token_max_length); + size_t token_location = 0; + + enum mako_parse_state state = MAKO_PARSE_STATE_NORMAL; + const char *location = string; + + char ch; + while ((ch = *location++) != '\0') { + switch (state) { + case MAKO_PARSE_STATE_ESCAPE: + case MAKO_PARSE_STATE_QUOTE_ESCAPE: + token[token_location] = ch; + ++token_location; + state &= ~MAKO_PARSE_STATE_ESCAPE; // These work as a bitmask. + break; + + case MAKO_PARSE_STATE_QUOTE: + switch (ch) { + case '\\': + state = MAKO_PARSE_STATE_QUOTE_ESCAPE; + break; + case '"': + state = MAKO_PARSE_STATE_NORMAL; + break; + case ' ': + default: + token[token_location] = ch; + ++token_location; + } + break; + + case MAKO_PARSE_STATE_NORMAL: + switch (ch) { + case '\\': + state = MAKO_PARSE_STATE_ESCAPE; + break; + case '"': + state = MAKO_PARSE_STATE_QUOTE; + break; + case ' ': + // New token, apply the old one and reset our state. + if (!apply_criteria_field(criteria, token)) { + // An error should have been printed already. + return false; + } + memset(token, 0, token_max_length); + token_location = 0; + break; + default: + token[token_location] = ch; + ++token_location; + } + break; + } + } + + if (state != MAKO_PARSE_STATE_NORMAL) { + if (state & MAKO_PARSE_STATE_QUOTE) { + fprintf(stderr, "Unmatched quote in criteria definition\n"); + return false; + } else if (state & MAKO_PARSE_STATE_ESCAPE) { + fprintf(stderr, "Trailing backslash in criteria definition\n"); + return false; + } else { + fprintf(stderr, "Got confused parsing criteria definition\n"); + return false; + } + } + + // Apply the last token, which will be left in the buffer after we hit the + // final NULL. We know it's valid since we just checked for that. + if (!apply_criteria_field(criteria, token)) { + // An error should have been printed by this point, we don't need to. + return false; + } + + return true; +} + +// Takes a token from the criteria string that looks like "key=value", figures +// out which field of the criteria "key" refers to, and sets it to "value". +// Any further equal signs are assumed to be part of the value. If there is no . +// equal sign present, the field is treated as a boolean, with a leading +// exclamation point signifying negation. +// +// Note that the token will be consumed. +bool apply_criteria_field(struct mako_criteria *criteria, char *token) { + char *key = token; + char *value = strstr(key, "="); + bool bare_key = !value; + + if (*key == '\0') { + return true; + } + + if (value) { + // Skip past the equal sign to the value itself. + *value = '\0'; + ++value; + } else { + // If there's no value, assume it's a boolean, and set the value + // appropriately. This allows uniform parsing later on. + if (*key == '!') { + // Negated boolean, skip past the exclamation point. + ++key; + value = "false"; + } else { + value = "true"; + } + } + + // Now apply the value to the appropriate member of the criteria. + // If the value was omitted, only try to match against boolean fields. + // Otherwise, anything is fair game. This helps to return a better error + // message. + + if (!bare_key) { + if (strcmp(key, "app-name") == 0) { + criteria->app_name = strdup(value); + criteria->spec.app_name = true; + return true; + } else if (strcmp(key, "app-icon") == 0) { + criteria->app_icon = strdup(value); + criteria->spec.app_icon = true; + return true; + } else if (strcmp(key, "urgency") == 0) { + if (!parse_urgency(value, &criteria->urgency)) { + fprintf(stderr, "Invalid urgency value '%s'", value); + return false; + } + criteria->spec.urgency = true; + return true; + } else if (strcmp(key, "category") == 0) { + criteria->category = strdup(value); + criteria->spec.category = true; + return true; + } else if (strcmp(key, "desktop-entry") == 0) { + criteria->desktop_entry = strdup(value); + criteria->spec.desktop_entry = true; + return true; + } else { + // Anything left must be one of the boolean fields, defined using + // standard syntax. Continue on. + } + } + + if (strcmp(key, "actionable") == 0) { + if (!parse_boolean(value, &criteria->actionable)) { + fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", + value, key); + return false; + } + criteria->spec.actionable = true; + return true; + } else { + if (bare_key) { + fprintf(stderr, "Invalid boolean criteria field '%s'\n", key); + } else { + fprintf(stderr, "Invalid criteria field '%s'\n", key); + } + return false; + } + + return true; +} + +// Retreive the global critiera from a given mako_config. This just so happens +// to be the first criteria in the list. +struct mako_criteria *global_criteria(struct mako_config *config) { + struct mako_criteria *criteria = + wl_container_of(config->criteria.next, criteria, link); + return criteria; +} + + +// Iterate through `criteria_list`, applying the style from each matching +// criteria to `notif`. Returns the number of criteria that matched, or -1 if +// a failure occurs. +ssize_t apply_each_criteria(struct wl_list *criteria_list, + struct mako_notification *notif) { + ssize_t match_count = 0; + + struct mako_criteria *criteria; + wl_list_for_each(criteria, criteria_list, link) { + if (!match_criteria(criteria, notif)) { + continue; + } + ++match_count; + + if (!apply_style(¬if->style, &criteria->style)) { + return -1; + } + } + + return match_count; +} diff --git a/dbus/xdg.c b/dbus/xdg.c index 281c1d39..e5739a15 100644 --- a/dbus/xdg.c +++ b/dbus/xdg.c @@ -4,6 +4,7 @@ #include #include +#include "criteria.h" #include "dbus.h" #include "mako.h" #include "notification.h" @@ -27,21 +28,21 @@ static int handle_get_capabilities(sd_bus_message *msg, void *data, return ret; } - if (strstr(state->config.style.format, "%b") != NULL) { + if (strstr(global_criteria(&state->config)->style.format, "%b") != NULL) { ret = sd_bus_message_append(reply, "s", "body"); if (ret < 0) { return ret; } } - if (state->config.style.markup) { + if (global_criteria(&state->config)->style.markup) { ret = sd_bus_message_append(reply, "s", "body-markup"); if (ret < 0) { return ret; } } - if (state->config.style.actions) { + if (global_criteria(&state->config)->style.actions) { ret = sd_bus_message_append(reply, "s", "actions"); if (ret < 0) { return ret; @@ -212,8 +213,22 @@ static int handle_notify(sd_bus_message *msg, void *data, return ret; } - if (expire_timeout < 0 || state->config.style.ignore_timeout) { - expire_timeout = state->config.style.default_timeout; + int match_count = apply_each_criteria(&state->config.criteria, notif); + if (match_count == -1) { + // We encountered an allocation failure or similar while applying + // criteria. The notification may be partially matched, but the worst + // case is that it has an empty style, so bail. + fprintf(stderr, "Failed to apply criteria\n"); + return -1; + } else if (match_count == 0) { + // This should be impossible, since the global criteria is always + // present in a mako_config and matches everything. + fprintf(stderr, "Notification matched zero criteria?!\n"); + return -1; + } + + if (expire_timeout < 0 || notif->style.ignore_timeout) { + expire_timeout = notif->style.default_timeout; } insert_notification(state, notif); diff --git a/include/config.h b/include/config.h index c5bc4290..fbf9a389 100644 --- a/include/config.h +++ b/include/config.h @@ -3,6 +3,7 @@ #include #include +#include struct mako_directional { int32_t top; @@ -28,7 +29,7 @@ enum mako_sort_criteria { // structs are also mirrored. struct mako_style_spec { bool width, height, margin, padding, border_size, font, markup, format, - actions, default_timeout; + actions, default_timeout, ignore_timeout; struct { bool background, text, border; @@ -60,14 +61,15 @@ struct mako_style { }; struct mako_config { - struct mako_style style; + struct wl_list criteria; // mako_criteria::link int32_t max_visible; char *output; - char *hidden_format; uint32_t sort_criteria; //enum mako_sort_criteria uint32_t sort_asc; + struct mako_style hidden_style; + struct { enum mako_button_binding left, right, middle; } button_bindings; @@ -77,7 +79,9 @@ void init_default_config(struct mako_config *config); void finish_config(struct mako_config *config); void init_default_style(struct mako_style *style); +void init_empty_style(struct mako_style *style); void finish_style(struct mako_style *style); +bool apply_style(struct mako_style *target, const struct mako_style *style); int parse_config_arguments(struct mako_config *config, int argc, char **argv); int load_config_file(struct mako_config *config); diff --git a/include/criteria.h b/include/criteria.h new file mode 100644 index 00000000..8f0d8ae5 --- /dev/null +++ b/include/criteria.h @@ -0,0 +1,65 @@ +#ifndef _MAKO_CRITERIA_H +#define _MAKO_CRITERIA_H + +#include +#include +#include "config.h" +#include "notification.h" + +struct mako_config; + +// State is intended to work as a bitmask, so if more need to be added in the +// future, this should be taken into account. +enum mako_parse_state { + MAKO_PARSE_STATE_NORMAL = 0, + MAKO_PARSE_STATE_ESCAPE = 1, + MAKO_PARSE_STATE_QUOTE = 2, + MAKO_PARSE_STATE_QUOTE_ESCAPE = 3, +}; + +// Stores whether or not each field was part of the criteria specification, so +// that, for example, "not actionable" can be distinguished from "don't care". +// This is unnecessary for string fields, but it's best to just keep it +// consistent. +struct mako_criteria_spec { + bool app_name; + bool app_icon; + bool actionable; + bool urgency; + bool category; + bool desktop_entry; +}; + +struct mako_criteria { + struct mako_criteria_spec spec; + struct wl_list link; // mako_config::criteria + + // Style to apply to matches: + struct mako_style style; + + // Fields that can be matched: + char *app_name; + char *app_icon; + bool actionable; // Whether mako_notification.actions is nonempty + + enum mako_notification_urgency urgency; + char *category; + char *desktop_entry; +}; + +struct mako_criteria *create_criteria(struct mako_config *config); +void destroy_criteria(struct mako_criteria *criteria); +bool match_criteria(struct mako_criteria *criteria, + struct mako_notification *notif); + +bool parse_boolean(const char *string, bool *out); +bool parse_urgency(const char *string, enum mako_notification_urgency *out); + +bool parse_criteria(const char *string, struct mako_criteria *criteria); +bool apply_criteria_field(struct mako_criteria *criteria, char *token); + +struct mako_criteria *global_criteria(struct mako_config *config); +ssize_t apply_each_criteria(struct wl_list *criteria_list, + struct mako_notification *notif); + +#endif diff --git a/include/notification.h b/include/notification.h index 1bd3cb67..2296fa90 100644 --- a/include/notification.h +++ b/include/notification.h @@ -5,6 +5,8 @@ #include #include +#include "config.h" + enum mako_notification_urgency { MAKO_NOTIFICATION_URGENCY_LOW = 0, MAKO_NOTIFICATION_URGENCY_NORMAL = 1, @@ -24,6 +26,8 @@ struct mako_notification { struct mako_state *state; struct wl_list link; // mako_state::notifications + struct mako_style style; + uint32_t id; char *app_name; char *app_icon; diff --git a/mako.1.scd b/mako.1.scd index c9da007b..6586223e 100644 --- a/mako.1.scd +++ b/mako.1.scd @@ -19,6 +19,31 @@ dismissed with a click or via *makoctl*(1). *-h, --help* Show help message and quit. +# GLOBAL CONFIGURATION OPTIONS + +*--max-visible* _n_ + Set maximum number of visible notifications to _n_. Older notifications will + be hidden. If -1, all notifications are visible. + + Default: 5 + +*--sort* _+/-time_ | _+/-priority_ + Sorts incoming notifications by time and/or priority in ascending(+) + or descending(-) order. + + Default: -time + +*--output* _name_ + Show notifications on the specified output. If empty, notifications will + appear on the focused output. + + Requires the compositor to support the Wayland protocol + xdg-output-unstable-v1 version 2. + + Default: "" + +# STYLE OPTIONS + *--font* _font_ Set font to _font_, in Pango format. @@ -79,19 +104,6 @@ dismissed with a click or via *makoctl*(1). Default: %s\\n%b -*--max-visible* _n_ - Set maximum number of visible notifications to _n_. Older notifications will - be hidden. If -1, all notifications are visible. - - Default: 5 - -*--sort* _+/-time_ | _+/-priority_ - Sorts incoming notifications by time and/or priority in ascending(+) - or descending(-) order. - - Default: -time - - *--default-timeout* _timeout_ Set the default timeout to _timeout_ in milliseconds. To disable the timeout, set it to zero. @@ -104,15 +116,6 @@ dismissed with a click or via *makoctl*(1). Default: 0 -*--output* _name_ - Show notifications on the specified output. If empty, notifications will - appear on the focused output. - - Requires the compositor to support the Wayland protocol - xdg-output-unstable-v1 version 2. - - Default: "" - # CONFIG FILE The config file is located at *~/.config/mako/config* or at @@ -124,11 +127,52 @@ Is equivalent to passing *--key=value* to mako from the command line. Empty lines and lines that begin with # are ignored. -The hidden notifications placeholder can be configured after a *[hidden]* -section: +# CRITERIA + +In addition to the set of options at the top of the file, the config file may +contain zero or more sections, each containing any combination of the +*STYLE OPTIONS*. The sections, called criteria, are defined with an INI-like +square bracket syntax. The brackets may contain any number of fields, like so: + + \[field=value field2=value2 ...\] + +When a notification is received, it will be compared to the fields defined in +each criteria. If all of the fields match, the style options within will be +applied to the notification. Fields not included in the criteria are not +considered during the match. A notification may match any number of criteria. +This matching occurs in the order the criteria are defined in the config file, +meaning that if multiple criteria match a notification, the last occurrence of +any given style option will "win". + +The following fields are available in critiera: + +- _app-name_ (string) +- _app-icon_ (string) +- _urgency_ (one of "low", "normal", "high") +- _category_ (string) +- _desktop-entry_ (string) +- _actionable_ (boolean) +- _hidden_ (boolean) + - _hidden_ is special, it defines the style for the placeholder shown when + the number of notifications exceeds _max-visible_. + +If a field's value contains special characters, they may be escaped with a +backslash, or quoted: + + \[app-name="Google Chrome"\] + + \[app-name=Google\\ Chrome\] + +Quotes within quotes may also be escaped, and a literal backslack may be +specified as \\\\. No spaces are allowed around the equal sign. Escaping equal +signs within values is unnecessary. + +Additionally, boolean values may be specified using any of true/false, 0/1, or +as bare words: + + \[actionable=true\] \[actionable=1\] \[actionable\] - \[hidden\] - format=(%h more) + \[actionable=false\] \[actionable=0\] \[!actionable\] # COLORS diff --git a/meson.build b/meson.build index a65ecb4a..d163aa18 100644 --- a/meson.build +++ b/meson.build @@ -38,6 +38,7 @@ executable( 'pool-buffer.c', 'render.c', 'wayland.c', + 'criteria.c', ]), dependencies: [ cairo, diff --git a/notification.c b/notification.c index 92f1c120..50ce528f 100644 --- a/notification.c +++ b/notification.c @@ -10,6 +10,7 @@ #include #endif +#include "config.h" #include "dbus.h" #include "event-loop.h" #include "mako.h" @@ -49,6 +50,7 @@ void destroy_notification(struct mako_notification *notif) { free(action); } destroy_timer(notif->timer); + finish_style(¬if->style); free(notif->app_name); free(notif->app_icon); free(notif->summary); @@ -212,7 +214,7 @@ size_t format_text(const char *format, char *buf, mako_format_func_t format_func char *value = NULL; bool markup = false; - if (current[1] == '%') { + if (current[1] == '%') { value = strdup("%"); } else { value = format_func(current[1], &markup, data); diff --git a/render.c b/render.c index f403df19..9da10e72 100644 --- a/render.c +++ b/render.c @@ -3,6 +3,8 @@ #include #include +#include "config.h" +#include "criteria.h" #include "mako.h" #include "notification.h" #include "render.h" @@ -114,7 +116,6 @@ static int render_notification(cairo_t *cairo, struct mako_state *state, int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { struct mako_config *config = &state->config; - struct mako_style *style = &config->style; cairo_t *cairo = buffer->cairo; if (wl_list_empty(&state->notifications)) { @@ -128,17 +129,17 @@ int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { cairo_paint(cairo); cairo_restore(cairo); - int inner_margin = style->margin.top; - if (style->margin.bottom > style->margin.top) { - inner_margin = style->margin.bottom; - } - int notif_width = state->width; size_t i = 0; int total_height = 0; + int pending_bottom_margin = 0; struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { + // Note that by this point, everything in the style is guaranteed to + // be specified, so we don't need to check. + struct mako_style *style = ¬if->style; + size_t text_len = format_text(style->format, NULL, format_notif_text, notif); char *text = malloc(text_len + 1); @@ -148,7 +149,11 @@ int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { format_text(style->format, text, format_notif_text, notif); if (i > 0) { - total_height += inner_margin; + if (style->margin.top > pending_bottom_margin) { + total_height += style->margin.top; + } else { + total_height += pending_bottom_margin; + } } int notif_height = render_notification( @@ -162,6 +167,7 @@ int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { notif->hotspot.height = notif_height; total_height += notif_height; + pending_bottom_margin = style->margin.bottom; ++i; if (config->max_visible >= 0 && @@ -171,20 +177,33 @@ int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { } if (wl_list_length(&state->notifications) > config->max_visible) { - total_height += inner_margin; + // Apply the hidden_style on top of the global style. This has to be + // done here since this notification isn't "real" and wasn't processed + // by apply_each_criteria. + struct mako_style style; + init_empty_style(&style); + apply_style(&style, &global_criteria(config)->style); + apply_style(&style, &config->hidden_style); + + if (style.margin.top > pending_bottom_margin) { + total_height += style.margin.top; + } else { + total_height += pending_bottom_margin; + } size_t text_ln = - format_text(config->hidden_format, NULL, format_state_text, state); + format_text(style.format, NULL, format_state_text, state); char *text = malloc(text_ln + 1); if (text == NULL) { fprintf(stderr, "allocation failed"); return 0; } - format_text(config->hidden_format, text, format_state_text, state); + format_text(style.format, text, format_state_text, state); int hidden_height = render_notification( - cairo, state, style, text, total_height, scale); + cairo, state, &style, text, total_height, scale); free(text); + finish_style(&style); total_height += hidden_height; } diff --git a/wayland.c b/wayland.c index 2a235c4c..699237e6 100644 --- a/wayland.c +++ b/wayland.c @@ -3,6 +3,7 @@ #include #include +#include "criteria.h" #include "mako.h" #include "notification.h" #include "render.h" @@ -407,14 +408,15 @@ void send_frame(struct mako_state *state) { zwlr_layer_surface_v1_add_listener(state->layer_surface, &layer_surface_listener, state); - struct mako_config *config = &state->config; - zwlr_layer_surface_v1_set_size(state->layer_surface, - config->style.width, height); + struct mako_style *style = &global_criteria(&state->config)->style; + + zwlr_layer_surface_v1_set_size(state->layer_surface, style->width, + height); zwlr_layer_surface_v1_set_anchor(state->layer_surface, ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); zwlr_layer_surface_v1_set_margin(state->layer_surface, - config->style.margin.top, config->style.margin.right, - config->style.margin.bottom, config->style.margin.left); + style->margin.top, style->margin.right, + style->margin.bottom, style->margin.left); wl_surface_commit(state->surface); return; } @@ -427,7 +429,7 @@ void send_frame(struct mako_state *state) { // requested, we'll enter an infinite loop if (state->height != height) { zwlr_layer_surface_v1_set_size(state->layer_surface, - state->config.style.width, height); + global_criteria(&state->config)->style.width, height); wl_surface_commit(state->surface); return; }