pax_global_header00006660000000000000000000000064135143207750014521gustar00rootroot0000000000000052 comment=e8e4c4d5ab96414b4b05a5f7138187998f53ebf9 mako-notifier-1.4/000077500000000000000000000000001351432077500141315ustar00rootroot00000000000000mako-notifier-1.4/.build.yml000066400000000000000000000006211351432077500160300ustar00rootroot00000000000000image: archlinux packages: - meson - wayland - wayland-protocols - cairo - pango - gdk-pixbuf2 - scdoc - systemd sources: - https://github.com/emersion/mako tasks: - setup: | cd mako meson build -Dauto_features=enabled - build: | cd mako ninja -C build - build-no-icons: | cd mako meson configure build -Dicons=disabled ninja -C build mako-notifier-1.4/.editorconfig000066400000000000000000000002011351432077500165770ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_style = tab insert_final_newline = true trim_trailing_whitespace = true mako-notifier-1.4/.gitignore000066400000000000000000000007101351432077500161170ustar00rootroot00000000000000# Prerequisites *.d # Object files *.o *.ko *.obj *.elf # Linker output *.ilk *.map *.exp # Precompiled Headers *.gch *.pch # Libraries *.lib *.a *.la *.lo # Shared objects (inc. Windows DLLs) *.dll *.so *.so.* *.dylib # Executables *.exe *.out *.app *.i*86 *.x86_64 *.hex # Debug files *.dSYM/ *.su *.idb *.pdb # vim ctags tags # Kernel Module Compile Results *.mod* *.cmd .tmp_versions/ modules.order Module.symvers Mkfile.old dkms.conf build/ mako-notifier-1.4/LICENSE000066400000000000000000000020511351432077500151340ustar00rootroot00000000000000MIT License Copyright (c) 2018 emersion Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mako-notifier-1.4/README.md000066400000000000000000000015611351432077500154130ustar00rootroot00000000000000# mako A lightweight notification daemon for Wayland. Works on Sway.

mako screenshot

Feel free to join the IRC channel: ##emersion on irc.freenode.net. ## Running If you're using Sway you can start mako on launch by putting `exec mako` in your configuration file. If you are using elogind, you might need to manually start a dbus user session: `dbus-daemon --session --address=unix:path=$XDG_RUNTIME_DIR/bus` ## Building Install dependencies: * meson (build-time dependency) * wayland * pango * cairo * systemd or elogind (for the sd-bus library) * gdk-pixbuf (optional, for icons support) * dbus (runtime dependency, user-session support is required) Then run: ```shell meson build ninja -C build build/mako ```

mako

## License MIT mako-notifier-1.4/cairo-pixbuf.c000066400000000000000000000047261351432077500166760ustar00rootroot00000000000000#include "cairo-pixbuf.h" cairo_surface_t *create_cairo_surface_from_gdk_pixbuf(const GdkPixbuf *gdkbuf) { int chan = gdk_pixbuf_get_n_channels(gdkbuf); if (chan < 3) { return NULL; } const guint8* gdkpix = gdk_pixbuf_read_pixels(gdkbuf); if (!gdkpix) { return NULL; } gint w = gdk_pixbuf_get_width(gdkbuf); gint h = gdk_pixbuf_get_height(gdkbuf); int stride = gdk_pixbuf_get_rowstride(gdkbuf); cairo_format_t fmt = (chan == 3) ? CAIRO_FORMAT_RGB24 : CAIRO_FORMAT_ARGB32; cairo_surface_t * cs = cairo_image_surface_create(fmt, w, h); cairo_surface_flush(cs); if (!cs || cairo_surface_status(cs) != CAIRO_STATUS_SUCCESS) { return NULL; } int cstride = cairo_image_surface_get_stride(cs); unsigned char *cpix = cairo_image_surface_get_data(cs); if (chan == 3) { for (int i = h; i; --i) { const guint8 *gp = gdkpix; unsigned char *cp = cpix; const guint8* end = gp + 3*w; while (gp < end) { #if G_BYTE_ORDER == G_LITTLE_ENDIAN cp[0] = gp[2]; cp[1] = gp[1]; cp[2] = gp[0]; #else cp[1] = gp[0]; cp[2] = gp[1]; cp[3] = gp[2]; #endif gp += 3; cp += 4; } gdkpix += stride; cpix += cstride; } } else { /* premul-color = alpha/255 * color/255 * 255 = (alpha*color)/255 * (z/255) = z/256 * 256/255 = z/256 (1 + 1/255) * = z/256 + (z/256)/255 = (z + z/255)/256 * # recurse once * = (z + (z + z/255)/256)/256 * = (z + z/256 + z/256/255) / 256 * # only use 16bit uint operations, loose some precision, * # result is floored. * -> (z + z>>8)>>8 * # add 0x80/255 = 0.5 to convert floor to round * => (z+0x80 + (z+0x80)>>8 ) >> 8 * ------ * tested as equal to lround(z/255.0) for uint z in [0..0xfe02] */ #define PREMUL_ALPHA(x,a,b,z) { z = a * b + 0x80; x = (z + (z >> 8)) >> 8; } for (int i = h; i; --i) { const guint8 *gp = gdkpix; unsigned char *cp = cpix; const guint8* end = gp + 4*w; guint z1, z2, z3; while (gp < end) { #if G_BYTE_ORDER == G_LITTLE_ENDIAN PREMUL_ALPHA(cp[0], gp[2], gp[3], z1); PREMUL_ALPHA(cp[1], gp[1], gp[3], z2); PREMUL_ALPHA(cp[2], gp[0], gp[3], z3); cp[3] = gp[3]; #else PREMUL_ALPHA(cp[1], gp[0], gp[3], z1); PREMUL_ALPHA(cp[2], gp[1], gp[3], z2); PREMUL_ALPHA(cp[3], gp[2], gp[3], z3); cp[0] = gp[3]; #endif gp += 4; cp += 4; } gdkpix += stride; cpix += cstride; } #undef PREMUL_ALPHA } cairo_surface_mark_dirty(cs); return cs; } mako-notifier-1.4/config.c000066400000000000000000000527041351432077500155520ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include "config.h" #include "criteria.h" #include "types.h" static int32_t max(int32_t a, int32_t b) { return (a > b) ? a : b; } void init_default_config(struct mako_config *config) { wl_list_init(&config->criteria); struct mako_criteria *new_criteria = create_criteria(config); init_default_style(&new_criteria->style); new_criteria->raw_string = strdup("(root)"); // Hide grouped notifications by default, and put the group count in // their format... new_criteria = create_criteria(config); init_empty_style(&new_criteria->style); new_criteria->grouped = true; new_criteria->spec.grouped = true; new_criteria->style.invisible = true; new_criteria->style.spec.invisible = true; new_criteria->style.format = strdup("(%g) %s\n%b"); new_criteria->style.spec.format = true; new_criteria->raw_string = strdup("(default grouped)"); // ...but make the first one in the group visible. new_criteria = create_criteria(config); init_empty_style(&new_criteria->style); new_criteria->group_index = 0; new_criteria->spec.group_index = true; new_criteria->style.invisible = false; new_criteria->style.spec.invisible = true; new_criteria->raw_string = strdup("(default group-index=0)"); init_empty_style(&config->superstyle); init_empty_style(&config->hidden_style); config->hidden_style.format = strdup("(%h more)"); config->hidden_style.spec.format = true; config->output = strdup(""); config->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; config->max_visible = 5; config->sort_criteria = MAKO_SORT_CRITERIA_TIME; config->sort_asc = 0; config->button_bindings.left = MAKO_BINDING_INVOKE_DEFAULT_ACTION; config->button_bindings.right = MAKO_BINDING_DISMISS; config->button_bindings.middle = MAKO_BINDING_NONE; config->touch = MAKO_BINDING_DISMISS; config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } void finish_config(struct mako_config *config) { struct mako_criteria *criteria, *tmp; wl_list_for_each_safe(criteria, tmp, &config->criteria, link) { destroy_criteria(criteria); } finish_style(&config->superstyle); finish_style(&config->hidden_style); free(config->output); } void init_default_style(struct mako_style *style) { style->width = 300; style->height = 100; style->margin.top = 10; style->margin.right = 10; style->margin.bottom = 10; style->margin.left = 10; style->padding.top = 5; style->padding.right = 5; style->padding.bottom = 5; style->padding.left = 5; style->border_size = 2; style->border_radius = 0; #ifdef HAVE_ICONS style->icons = true; #else style->icons = false; #endif style->max_icon_size = 64; style->icon_path = strdup(""); // hicolor and pixmaps are implicit. style->font = strdup("monospace 10"); style->markup = true; style->format = strdup("%s\n%b"); style->actions = true; style->default_timeout = 0; style->ignore_timeout = false; style->colors.background = 0x285577FF; style->colors.text = 0xFFFFFFFF; style->colors.border = 0x4C7899FF; style->colors.progress.value = 0x5588AAFF; style->colors.progress.operator = CAIRO_OPERATOR_OVER; style->group_criteria_spec.none = true; style->invisible = false; // Everything in the default config is explicitly specified. 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; char *new_icon_path = 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) { free(new_font); fprintf(stderr, "allocation failed\n"); return false; } } if (style->spec.icon_path) { new_icon_path = strdup(style->icon_path); if (new_icon_path == NULL) { free(new_format); free(new_font); 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.icons) { target->icons = style->icons; target->spec.icons = true; } if (style->spec.max_icon_size) { target->max_icon_size = style->max_icon_size; target->spec.max_icon_size = true; } if (style->spec.icon_path) { free(target->icon_path); target->icon_path = new_icon_path; target->spec.icon_path = 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; } if (style->spec.colors.progress) { target->colors.progress = style->colors.progress; target->spec.colors.progress = true; } if (style->spec.group_criteria_spec) { target->group_criteria_spec = style->group_criteria_spec; target->spec.group_criteria_spec = true; } if (style->spec.invisible) { target->invisible = style->invisible; target->spec.invisible = true; } if (style->border_radius) { target->border_radius = style->border_radius; target->spec.border_radius = true; } return true; } // Given a config and a style in which to store the information, this will // calculate a style that has the maximum value of all the configured criteria // styles (including the default as a base), for values where it makes sense to // have a maximum. Those that don't make sense will be unchanged. Usually, you // want to pass an empty style as the target. bool apply_superset_style( struct mako_style *target, struct mako_config *config) { // Specify eveything that we'll be combining. target->spec.width = true; target->spec.height = true; target->spec.margin = true; target->spec.padding = true; target->spec.border_size = true; target->spec.icons = true; target->spec.max_icon_size = true; target->spec.default_timeout = true; target->spec.markup = true; target->spec.actions = true; target->spec.format = true; free(target->format); // The "format" needs enough space for one of each specifier. target->format = calloc(1, (2 * strlen(VALID_FORMAT_SPECIFIERS)) + 1); char *target_format_pos = target->format; // Now we loop over the criteria and add together those fields. // We can't use apply_style, because it simply overwrites each field. struct mako_criteria *criteria; wl_list_for_each(criteria, &config->criteria, link) { struct mako_style *style = &criteria->style; // We can cheat and skip checking whether any of these are specified, // since we're looking for the max and unspecified ones will be // initialized to zero. target->width = max(style->width, target->width); target->height = max(style->height, target->height); target->margin.top = max(style->margin.top, target->margin.top); target->margin.right = max(style->margin.right, target->margin.right); target->margin.bottom = max(style->margin.bottom, target->margin.bottom); target->margin.left = max(style->margin.left, target->margin.left); target->padding.top = max(style->padding.top, target->padding.top); target->padding.right = max(style->padding.right, target->padding.right); target->padding.bottom = max(style->padding.bottom, target->padding.bottom); target->padding.left = max(style->padding.left, target->padding.left); target->border_size = max(style->border_size, target->border_size); target->icons = style->icons || target->icons; target->max_icon_size = max(style->max_icon_size, target->max_icon_size); target->default_timeout = max(style->default_timeout, target->default_timeout); target->markup |= style->markup; target->actions |= style->actions; // We do need to be safe about this one though. if (style->spec.format) { char *format_pos = style->format; char current_specifier[3] = {0}; while (*format_pos) { format_pos = strstr(format_pos, "%"); if (!format_pos) { break; } // We only want to add the format specifier to the target if we // haven't already seen it. // Need to copy the specifier into its own string to use strstr // here, because there's no way to limit how much of the string // it uses in the comparison. memcpy(¤t_specifier, format_pos, 2); if (!strstr(target->format, current_specifier)) { memcpy(target_format_pos, format_pos, 2); } ++format_pos; // Enough to move to the next match. target_format_pos += 2; // This needs to go to the next slot. } } } return true; } 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, "layer") == 0) { if (strcmp(value, "background") == 0) { config->layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; } else if (strcmp(value, "bottom") == 0) { config->layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; } else if (strcmp(value, "top") == 0) { config->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; } else if (strcmp(value, "overlay") == 0) { config->layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; } else { return false; } 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; } else { return false; } return true; } else if (strcmp(name, "anchor") == 0) { if (strcmp(value, "top-right") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } else if (strcmp(value, "top-center") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; } else if (strcmp(value, "top-left") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; } else if (strcmp(value, "bottom-right") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } else if (strcmp(value, "bottom-center") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; } else if (strcmp(value, "bottom-left") == 0) { config->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; } else if (strcmp(value, "center") == 0) { config->anchor = 0; } else { return false; } return true; } 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); return spec->font = !!(style->font = strdup(value)); } else if (strcmp(name, "background-color") == 0) { return spec->colors.background = parse_color(value, &style->colors.background); } else if (strcmp(name, "text-color") == 0) { return spec->colors.text = parse_color(value, &style->colors.text); } else if (strcmp(name, "width") == 0) { return spec->width = parse_int(value, &style->width); } else if (strcmp(name, "height") == 0) { return spec->height = parse_int(value, &style->height); } else if (strcmp(name, "margin") == 0) { return spec->margin = parse_directional(value, &style->margin); } else if (strcmp(name, "padding") == 0) { spec->padding = parse_directional(value, &style->padding); if (spec->border_radius && spec->padding) { style->padding.left = max(style->border_radius, style->padding.left); style->padding.right = max(style->border_radius, style->padding.right); } return spec->padding; } else if (strcmp(name, "border-size") == 0) { return spec->border_size = parse_int(value, &style->border_size); } else if (strcmp(name, "border-color") == 0) { return spec->colors.border = parse_color(value, &style->colors.border); } else if (strcmp(name, "progress-color") == 0) { return spec->colors.progress = parse_mako_color(value, &style->colors.progress); } else if (strcmp(name, "icons") == 0) { #ifdef HAVE_ICONS return spec->icons = parse_boolean(value, &style->icons); #else fprintf(stderr, "Icon support not built in, ignoring icons setting.\n"); return true; #endif } else if (strcmp(name, "max-icon-size") == 0) { return spec->max_icon_size = parse_int(value, &style->max_icon_size); } else if (strcmp(name, "icon-path") == 0) { free(style->icon_path); return spec->icon_path = !!(style->icon_path = strdup(value)); } else if (strcmp(name, "markup") == 0) { return spec->markup = parse_boolean(value, &style->markup); } else if (strcmp(name, "actions") == 0) { return spec->actions = parse_boolean(value, &style->actions); } else if (strcmp(name, "format") == 0) { free(style->format); return spec->format = parse_format(value, &style->format); } else if (strcmp(name, "default-timeout") == 0) { return spec->default_timeout = parse_int(value, &style->default_timeout); } else if (strcmp(name, "ignore-timeout") == 0) { return spec->ignore_timeout = parse_boolean(value, &style->ignore_timeout); } else if (strcmp(name, "group-by") == 0) { return spec->group_criteria_spec = parse_criteria_spec(value, &style->group_criteria_spec); } else if (strcmp(name, "invisible") == 0) { return spec->invisible = parse_boolean(value, &style->invisible); } else if (strcmp(name, "border-radius") == 0) { spec->border_radius = parse_int(value, &style->border_radius); if (spec->border_radius && spec->padding) { style->padding.left = max(style->border_radius, style->padding.left); style->padding.right = max(style->border_radius, style->padding.right); } return spec->border_radius; } return false; } static bool file_exists(const char *path) { return path && access(path, R_OK) != -1; } static char *get_config_path(void) { static const char *config_paths[] = { "$HOME/.mako/config", "$XDG_CONFIG_HOME/mako/config", }; if (!getenv("XDG_CONFIG_HOME")) { char *home = getenv("HOME"); if (!home) { return NULL; } char config_home[strlen(home) + strlen("/.config") + 1]; strcpy(config_home, home); strcat(config_home, "/.config"); setenv("XDG_CONFIG_HOME", config_home, 1); } for (size_t i = 0; i < sizeof(config_paths) / sizeof(char *); ++i) { wordexp_t p; if (wordexp(config_paths[i], &p, 0) == 0) { char *path = strdup(p.we_wordv[0]); wordfree(&p); if (file_exists(path)) { return path; } free(path); } } return NULL; } int load_config_file(struct mako_config *config) { char *path = get_config_path(); if (!path) { return 0; } FILE *f = fopen(path, "r"); if (!f) { fprintf(stderr, "Unable to open %s for reading", path); free(path); return -1; } const char *base = basename(path); int ret = 0; int lineno = 0; char *line = NULL; char *section = NULL; // Until we hit the first criteria section, we want to be modifying the // root criteria's style. We know it's always the first one in the list. struct mako_criteria *criteria = wl_container_of(config->criteria.next, criteria, link); 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'; 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 { assert(criteria != NULL); 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); free(path); return ret; } int parse_config_arguments(struct mako_config *config, int argc, char **argv) { static const struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"font", required_argument, 0, 0}, {"background-color", required_argument, 0, 0}, {"text-color", required_argument, 0, 0}, {"width", required_argument, 0, 0}, {"height", required_argument, 0, 0}, {"margin", required_argument, 0, 0}, {"padding", required_argument, 0, 0}, {"border-size", required_argument, 0, 0}, {"border-color", required_argument, 0, 0}, {"border-radius", required_argument, 0, 0}, {"progress-color", required_argument, 0, 0}, {"icons", required_argument, 0, 0}, {"icon-path", required_argument, 0, 0}, {"max-icon-size", required_argument, 0, 0}, {"markup", required_argument, 0, 0}, {"actions", required_argument, 0, 0}, {"format", required_argument, 0, 0}, {"max-visible", required_argument, 0, 0}, {"default-timeout", required_argument, 0, 0}, {"ignore-timeout", required_argument, 0, 0}, {"output", required_argument, 0, 0}, {"layer", required_argument, 0, 0}, {"anchor", required_argument, 0, 0}, {"sort", required_argument, 0, 0}, {"group-by", required_argument, 0, 0}, {0}, }; struct mako_criteria *root_criteria = wl_container_of(config->criteria.next, root_criteria, link); optind = 1; while (1) { int option_index = -1; int c = getopt_long(argc, argv, "h", long_options, &option_index); if (c < 0) { break; } else if (c == 'h') { return 1; } else if (c != 0) { return -1; } const char *name = long_options[option_index].name; if (!apply_style_option(&root_criteria->style, name, optarg) && !apply_config_option(config, name, optarg)) { fprintf(stderr, "Failed to parse option '%s'\n", name); return -1; } } return 0; } // Returns zero on success, negative on error, positive if we should exit // immediately due to something the user asked for (like help). int reload_config(struct mako_config *config, int argc, char **argv) { struct mako_config new_config = {0}; init_default_config(&new_config); int config_status = load_config_file(&new_config); int args_status = parse_config_arguments(&new_config, argc, argv); if (args_status > 0) { finish_config(&new_config); return args_status; } else if (config_status < 0 || args_status < 0) { fprintf(stderr, "Failed to parse config\n"); finish_config(&new_config); return -1; } apply_superset_style(&new_config.superstyle, &new_config); finish_config(config); *config = new_config; // We have to rebuild the wl_list that contains the criteria, as it is // currently pointing to local memory instead of the location of the real // criteria struct. wl_list_init(&config->criteria); wl_list_insert_list(&config->criteria, &new_config.criteria); return 0; } mako-notifier-1.4/contrib/000077500000000000000000000000001351432077500155715ustar00rootroot00000000000000mako-notifier-1.4/contrib/apparmor/000077500000000000000000000000001351432077500174125ustar00rootroot00000000000000mako-notifier-1.4/contrib/apparmor/fr.emersion.Mako000066400000000000000000000010701351432077500224500ustar00rootroot00000000000000#include profile fr.emersion.Mako /usr/bin/mako { #include #include #include #include dbus bind bus=session name=org.freedesktop.Notifications, dbus receive bus=session path=/fr/emersion/Mako interface=fr.emersion.Mako, /{run,dev}/shm/mako-* rw, owner @{HOME}/.config/mako/config r, # Site-specific additions and overrides. See local/README for details. #include if exists } mako-notifier-1.4/contrib/apparmor/meson.build000066400000000000000000000003031351432077500215500ustar00rootroot00000000000000if get_option('apparmor') apparmordir = join_paths(get_option('sysconfdir'), 'apparmor.d') install_data( 'fr.emersion.Mako', install_dir: apparmordir, install_mode: 'rw-r-----', ) endif mako-notifier-1.4/contrib/completions/000077500000000000000000000000001351432077500201255ustar00rootroot00000000000000mako-notifier-1.4/contrib/completions/meson.build000066400000000000000000000002671351432077500222740ustar00rootroot00000000000000if get_option('zsh-completions') install_data( files( 'zsh/_makoctl', 'zsh/_mako', ), install_dir: datadir + '/zsh/site-functions', install_mode: 'rw-r--r--', ) endif mako-notifier-1.4/contrib/completions/zsh/000077500000000000000000000000001351432077500207315ustar00rootroot00000000000000mako-notifier-1.4/contrib/completions/zsh/_mako000066400000000000000000000034251351432077500217460ustar00rootroot00000000000000#compdef mako # Usage: mako [options...] # Colors can be specified with the format #RRGGBB or #RRGGBBAA. _arguments \ '-h[Show help message and quit.]' '--help[Show help message and quit.]' \ '--font[Font family and size.]:font:' \ '--background-color[Background color.]:color:' \ '--text-color[Text color.]:color:' \ '--width[Notification width.]:width:' \ '--height[Max notification height.]:height:' \ '--margin[Margin values, comma separated. Up to four values, with the same meaning as in CSS.]:margin:' \ '--padding[Padding values, comma separated. Up to four values, with the same meaning as in CSS.]:padding:' \ '--border-size[Border size.]:size:' \ '--border-color[Border color.]:color:' \ '--markup[Enable/disable markup.]:markup enabled:(0 1)' \ '--actions[Applications may request an action to be associated with activating a notification. Disabling this will cause mako to ignore these requests.]:action enabled:(0 1)' \ '--format[Format string.]:format:' \ '--hidden-format[Format string.]:format:' \ '--max-visible[Max number of visible notifications.]:visible notification:' \ '--default-timeout[Default timeout in milliseconds.]:timeout (ms):' \ '--ignore-timeout[If set, mako will ignore the expire timeout sent by notifications and use the one provided by default-timeout instead.]:Use default timeout:(0 1)' \ '--output[Show notifications on this output.]:name:' \ '--layer[Arrange notifications at this layer.]:layer:(background bottom top overlay)' \ '--anchor[Position on output to put notifications.]:position:(top-right bottom-right bottom-center bottom-left top-left top-center)' \ '--sort[Sorts incoming notifications by time and/or priority in ascending(+) or descending(-) order.]:sort pattern:' mako-notifier-1.4/contrib/completions/zsh/_makoctl000066400000000000000000000013671351432077500224540ustar00rootroot00000000000000#compdef makoctl local -a makoctl_cmds makoctl_cmds=( 'dismiss:Dismisses notification (first by default)' 'invoke:Invokes an action on the first notification. If action is not specified, invokes the default action' 'reload:Reloads the configuration file' 'help:Show help message and quit' ) if (( CURRENT == 2 )); then _describe 'makoctl command' makoctl_cmds else shift words (( CURRENT-- )) opt="${words[1]}" if (( CURRENT == 2 )); then case "${opt}" in dismiss) _arguments -s \ '(-a --all)'{-a,--all}'[Dimiss all notification]' ;; invoke) _message -e action 'action' ;; esac fi fi mako-notifier-1.4/criteria.c000066400000000000000000000244631351432077500161100ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include "enum.h" #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->summary); free(criteria->body); free(criteria->raw_string); free(criteria); } bool match_criteria(struct mako_criteria *criteria, struct mako_notification *notif) { struct mako_criteria_spec spec = criteria->spec; if (spec.none) { // `none` short-circuits all other criteria. return false; } 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.expiring && criteria->expiring != (notif->requested_timeout != 0)) { 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; } if (spec.summary && strcmp(criteria->summary, notif->summary) != 0) { return false; } if (spec.body && strcmp(criteria->body, notif->body) != 0) { return false; } if (spec.group_index && criteria->group_index != notif->group_index) { return false; } if (spec.grouped && criteria->grouped != (notif->group_index >= 0)) { return false; } return true; } 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; case MAKO_PARSE_STATE_FORMAT: // Unsupported state for this parser. assert(0); } } 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; } criteria->raw_string = strdup(string); 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 if (strcmp(key, "group-index") == 0) { if (!parse_int(value, &criteria->group_index)) { fprintf(stderr, "Invalid group-index value '%s'", value); return false; } criteria->spec.group_index = true; return true; } else if (strcmp(key, "summary") == 0) { // TODO: convert to regex, currently only exact matching criteria->summary = strdup(value); criteria->spec.summary = true; return true; } else { // TODO: body, once we support regex and they're useful. // 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 (strcmp(key, "expiring") == 0){ if (!parse_boolean(value, &criteria->expiring)) { fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", value, key); return false; } criteria->spec.expiring = true; return true; } else if (strcmp(key, "grouped") == 0) { if (!parse_boolean(value, &criteria->grouped)) { fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", value, key); return false; } criteria->spec.grouped = 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; } assert(false && "Criteria parser fell through"); } // Retreive the global criteria 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; } // Given a notification and a criteria spec, create a criteria that matches the // specified fields of that notification. Unlike create_criteria, this new // criteria will not be automatically inserted into the configuration. It is // instead intended to be used for comparing notifications. The spec will be // copied, so the caller is responsible for doing whatever it needs to do with // the original after the call completes. struct mako_criteria *create_criteria_from_notification( struct mako_notification *notif, struct mako_criteria_spec *spec) { struct mako_criteria *criteria = calloc(1, sizeof(struct mako_criteria)); if (criteria == NULL) { fprintf(stderr, "allocation failed\n"); return NULL; } memcpy(&criteria->spec, spec, sizeof(struct mako_criteria_spec)); // We only really need to copy the ones that are in the spec, but it // doesn't hurt anything to do the rest and it makes this code much nicer // to look at. criteria->app_name = strdup(notif->app_name); criteria->app_icon = strdup(notif->app_icon); criteria->actionable = !wl_list_empty(¬if->actions); criteria->expiring = (notif->requested_timeout != 0); criteria->urgency = notif->urgency; criteria->category = strdup(notif->category); criteria->desktop_entry = strdup(notif->desktop_entry); criteria->summary = strdup(notif->summary); criteria->body = strdup(notif->body); criteria->group_index = notif->group_index; criteria->grouped = (notif->group_index >= 0); return criteria; } mako-notifier-1.4/dbus/000077500000000000000000000000001351432077500150665ustar00rootroot00000000000000mako-notifier-1.4/dbus/dbus.c000066400000000000000000000022361351432077500161720ustar00rootroot00000000000000#include #include #include "dbus.h" #include "mako.h" static const char service_name[] = "org.freedesktop.Notifications"; bool init_dbus(struct mako_state *state) { int ret = 0; state->bus = NULL; state->xdg_slot = state->mako_slot = NULL; ret = sd_bus_open_user(&state->bus); if (ret < 0) { fprintf(stderr, "Failed to connect to user bus: %s\n", strerror(-ret)); goto error; } ret = init_dbus_xdg(state); if (ret < 0) { fprintf(stderr, "Failed to initialize XDG interface: %s\n", strerror(-ret)); goto error; } ret = init_dbus_mako(state); if (ret < 0) { fprintf(stderr, "Failed to initialize Mako interface: %s\n", strerror(-ret)); goto error; } ret = sd_bus_request_name(state->bus, service_name, 0); if (ret < 0) { fprintf(stderr, "Failed to acquire service name: %s\n", strerror(-ret)); if (ret == -EEXIST) { fprintf(stderr, "Is a notification daemon already running?\n"); } goto error; } return true; error: finish_dbus(state); return false; } void finish_dbus(struct mako_state *state) { sd_bus_slot_unref(state->xdg_slot); sd_bus_slot_unref(state->mako_slot); sd_bus_flush_close_unref(state->bus); } mako-notifier-1.4/dbus/mako.c000066400000000000000000000134331351432077500161650ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include "config.h" #include "criteria.h" #include "dbus.h" #include "mako.h" #include "notification.h" #include "wayland.h" static const char *service_path = "/fr/emersion/Mako"; static const char *service_interface = "fr.emersion.Mako"; static int handle_dismiss_all_notifications(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; close_all_notifications(state, MAKO_NOTIFICATION_CLOSE_DISMISSED); set_dirty(state); return sd_bus_reply_method_return(msg, ""); } static int handle_dismiss_last_notification(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; if (wl_list_empty(&state->notifications)) { goto done; } struct mako_notification *notif = wl_container_of(state->notifications.next, notif, link); close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED); set_dirty(state); done: return sd_bus_reply_method_return(msg, ""); } static int handle_invoke_action(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; uint32_t id = 0; const char *action_key; int ret = sd_bus_message_read(msg, "us", &id, &action_key); if (ret < 0) { return ret; } if (id == 0) { id = state->last_id; } if (wl_list_empty(&state->notifications)) { goto done; } struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (notif->id == id) { struct mako_action *action; wl_list_for_each(action, ¬if->actions, link) { if (strcmp(action->key, action_key) == 0) { notify_action_invoked(action); break; } } break; } } done: return sd_bus_reply_method_return(msg, ""); } static int handle_list_notifications(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; sd_bus_message *reply = NULL; int ret = sd_bus_message_new_method_return(msg, &reply); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(reply, 'a', "a{sv}"); if (ret < 0) { return ret; } struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { ret = sd_bus_message_open_container(reply, 'a', "{sv}"); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "app-name", "s", notif->app_name); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "app-icon", "s", notif->app_icon); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "summary", "s", notif->summary); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "body", "s", notif->body); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "id", "u", notif->id); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(reply, 'e', "sv"); if (ret < 0) { return ret; } ret = sd_bus_message_append_basic(reply, 's', "actions"); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(reply, 'v', "a{ss}"); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(reply, 'a', "{ss}"); if (ret < 0) { return ret; } struct mako_action *action; wl_list_for_each(action, ¬if->actions, link) { ret = sd_bus_message_append(reply, "{ss}", action->key, action->title); if (ret < 0) { return ret; } } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } ret = sd_bus_send(NULL, reply, NULL); if (ret < 0) { return ret; } sd_bus_message_unref(reply); return 0; } static int handle_reload(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; if (reload_config(&state->config, state->argc, state->argv) != 0) { sd_bus_error_set_const( ret_error, "fr.emersion.Mako.InvalidConfig", "Unable to parse configuration file"); return -1; } struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { // Reset the notifications' grouped state so that if criteria have been // removed they'll separate properly. notif->group_index = -1; finish_style(¬if->style); init_empty_style(¬if->style); apply_each_criteria(&state->config.criteria, notif); // Having to do this for every single notification really hurts... but // it does do The Right Thing (tm). struct mako_criteria *notif_criteria = create_criteria_from_notification( notif, ¬if->style.group_criteria_spec); if (!notif_criteria) { continue; } group_notifications(state, notif_criteria); free(notif_criteria); } set_dirty(state); return sd_bus_reply_method_return(msg, ""); } static const sd_bus_vtable service_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("DismissAllNotifications", "", "", handle_dismiss_all_notifications, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("DismissLastNotification", "", "", handle_dismiss_last_notification, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("InvokeAction", "us", "", handle_invoke_action, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ListNotifications", "", "aa{sv}", handle_list_notifications, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Reload", "", "", handle_reload, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END }; int init_dbus_mako(struct mako_state *state) { return sd_bus_add_object_vtable(state->bus, &state->mako_slot, service_path, service_interface, service_vtable, state); } mako-notifier-1.4/dbus/xdg.c000066400000000000000000000274411351432077500160240ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include "config.h" #include "criteria.h" #include "dbus.h" #include "mako.h" #include "notification.h" #include "wayland.h" #include "icon.h" static const char *service_path = "/org/freedesktop/Notifications"; static const char *service_interface = "org.freedesktop.Notifications"; static int handle_get_capabilities(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; sd_bus_message *reply = NULL; int ret = sd_bus_message_new_method_return(msg, &reply); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(reply, 'a', "s"); if (ret < 0) { return ret; } if (strstr(state->config.superstyle.format, "%b") != NULL) { ret = sd_bus_message_append(reply, "s", "body"); if (ret < 0) { return ret; } } if (state->config.superstyle.markup) { ret = sd_bus_message_append(reply, "s", "body-markup"); if (ret < 0) { return ret; } } if (state->config.superstyle.actions) { ret = sd_bus_message_append(reply, "s", "actions"); if (ret < 0) { return ret; } } if (state->config.superstyle.icons) { ret = sd_bus_message_append(reply, "s", "icon-static"); if (ret < 0) { return ret; } } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } ret = sd_bus_send(NULL, reply, NULL); if (ret < 0) { return ret; } sd_bus_message_unref(reply); return 0; } static void handle_notification_timer(void *data) { struct mako_notification *notif = data; notif->timer = NULL; struct mako_state *state = notif->state; close_notification(notif, MAKO_NOTIFICATION_CLOSE_EXPIRED); set_dirty(state); } static int handle_notify(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; int ret = 0; const char *app_name, *app_icon, *summary, *body; uint32_t replaces_id; ret = sd_bus_message_read(msg, "susss", &app_name, &replaces_id, &app_icon, &summary, &body); if (ret < 0) { return ret; } struct mako_notification *notif = NULL; if (replaces_id > 0) { notif = get_notification(state, replaces_id); } if (notif) { reset_notification(notif); } else { // Either we had no replaces_id, or the id given was invalid. Either // way, make a new notification. replaces_id = 0; // In case they got lucky and passed the next id. notif = create_notification(state); } if (notif == NULL) { return -1; } notif->app_name = strdup(app_name); notif->app_icon = strdup(app_icon); notif->summary = strdup(summary); notif->body = strdup(body); // These fields may not be filled, so make sure they're valid strings. notif->category = strdup(""); notif->desktop_entry = strdup(""); ret = sd_bus_message_enter_container(msg, 'a', "s"); if (ret < 0) { return ret; } while (1) { const char *action_key, *action_title; ret = sd_bus_message_read(msg, "ss", &action_key, &action_title); if (ret < 0) { return ret; } else if (ret == 0) { break; } struct mako_action *action = calloc(1, sizeof(struct mako_action)); if (action == NULL) { return -1; } action->notification = notif; action->key = strdup(action_key); action->title = strdup(action_title); wl_list_insert(¬if->actions, &action->link); } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); if (ret < 0) { return ret; } while (1) { ret = sd_bus_message_enter_container(msg, 'e', "sv"); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char *hint = NULL; ret = sd_bus_message_read(msg, "s", &hint); if (ret < 0) { return ret; } if (strcmp(hint, "urgency") == 0) { // Should be a byte but some clients (Chromium) send an uint32_t const char *contents = NULL; ret = sd_bus_message_peek_type(msg, NULL, &contents); if (ret < 0) { return ret; } if (strcmp(contents, "u") == 0) { uint32_t urgency = 0; ret = sd_bus_message_read(msg, "v", "u", &urgency); if (ret < 0) { return ret; } notif->urgency = urgency; } else { uint8_t urgency = 0; ret = sd_bus_message_read(msg, "v", "y", &urgency); if (ret < 0) { return ret; } notif->urgency = urgency; } } else if (strcmp(hint, "category") == 0) { const char *category = NULL; ret = sd_bus_message_read(msg, "v", "s", &category); if (ret < 0) { return ret; } free(notif->category); notif->category = strdup(category); } else if (strcmp(hint, "desktop-entry") == 0) { const char *desktop_entry = NULL; ret = sd_bus_message_read(msg, "v", "s", &desktop_entry); if (ret < 0) { return ret; } free(notif->desktop_entry); notif->desktop_entry = strdup(desktop_entry); } else if (strcmp(hint, "value") == 0) { int32_t progress = 0; ret = sd_bus_message_read(msg, "v", "i", &progress); if (ret < 0) { return ret; } notif->progress = progress; } else if (strcmp(hint, "image-data") == 0 || strcmp(hint, "icon_data") == 0) { ret = sd_bus_message_enter_container(msg, 'v', "(iiibiiay)"); if (ret < 0) { return ret; } ret = sd_bus_message_enter_container(msg, 'r', "iiibiiay"); if (ret < 0) { return ret; } struct mako_image_data *image_data = calloc(1, sizeof(struct mako_image_data)); if (image_data == NULL) { return -1; } ret = sd_bus_message_read(msg, "iiibii", &image_data->width, &image_data->height, &image_data->rowstride, &image_data->has_alpha, &image_data->bits_per_sample, &image_data->channels); if (ret < 0) { free(image_data); return ret; } // Calculate the expected useful data length without padding in last row // len = size before last row + size of last row // = (height - 1) * rowstride + width * ceil(channels * bits_pre_sample / 8.0) size_t image_len = (image_data->height - 1) * image_data->rowstride + image_data->width * ((image_data->channels * image_data->bits_per_sample + 7) / 8); uint8_t *data = calloc(image_len, sizeof(uint8_t)); if (data == NULL) { free(image_data); return -1; } ret = sd_bus_message_enter_container(msg, 'a', "y"); if (ret < 0) { free(data); free(image_data); return ret; } // Ignore the extra padding bytes in the last row if exist for (size_t index = 0; index < image_len; index++) { uint8_t tmp; ret = sd_bus_message_read(msg, "y", &tmp); if (ret < 0){ free(data); free(image_data); return ret; } data[index] = tmp; } image_data->data = data; if (notif->image_data != NULL) { free(notif->image_data->data); free(notif->image_data); } notif->image_data = image_data; ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } } else { ret = sd_bus_message_skip(msg, "v"); if (ret < 0) { return ret; } } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } int32_t requested_timeout; ret = sd_bus_message_read(msg, "i", &requested_timeout); if (ret < 0) { return ret; } notif->requested_timeout = requested_timeout; // We can insert a notification prior to matching criteria, because sort is // global. We also know that inserting a notification into the global list // regardless of the configured sort criteria places it in the correct // position relative to any of its potential group mates even before // knowing what criteria we will be grouping them by (proof left as an // exercise to the reader). if (replaces_id != notif->id) { // Only insert notifcations if they're actually new, to avoid creating // duplicates in the list. insert_notification(state, notif); } 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"); destroy_notification(notif); 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"); destroy_notification(notif); return -1; } int32_t expire_timeout = notif->requested_timeout; if (expire_timeout < 0 || notif->style.ignore_timeout) { expire_timeout = notif->style.default_timeout; } if (expire_timeout > 0) { notif->timer = add_event_loop_timer(&state->event_loop, expire_timeout, handle_notification_timer, notif); } if (notif->style.icons) { notif->icon = create_icon(notif); } // Now we need to perform the grouping based on the new notification's // group criteria specification (list of critera which must match). We // don't necessarily want to start with the new notification, as depending // on the sort criteria, there may be matching ones earlier in the list. // After this call, the matching notifications will be contiguous in the // list, and the first one that matches will always still be first. struct mako_criteria *notif_criteria = create_criteria_from_notification( notif, ¬if->style.group_criteria_spec); if (!notif_criteria) { destroy_notification(notif); return -1; } group_notifications(state, notif_criteria); free(notif_criteria); set_dirty(state); return sd_bus_reply_method_return(msg, "u", notif->id); } static int handle_close_notification(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; uint32_t id; int ret = sd_bus_message_read(msg, "u", &id); if (ret < 0) { return ret; } // TODO: check client struct mako_notification *notif = get_notification(state, id); if (notif) { close_notification(notif, MAKO_NOTIFICATION_CLOSE_REQUEST); set_dirty(state); } return sd_bus_reply_method_return(msg, ""); } static int handle_get_server_information(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { const char *name = "mako"; const char *vendor = "emersion"; const char *version = "0.0.0"; const char *spec_version = "1.2"; return sd_bus_reply_method_return(msg, "ssss", name, vendor, version, spec_version); } static const sd_bus_vtable service_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("GetCapabilities", "", "as", handle_get_capabilities, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Notify", "susssasa{sv}i", "u", handle_notify, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("CloseNotification", "u", "", handle_close_notification, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("GetServerInformation", "", "ssss", handle_get_server_information, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_SIGNAL("ActionInvoked", "us", 0), SD_BUS_SIGNAL("NotificationClosed", "uu", 0), SD_BUS_VTABLE_END }; int init_dbus_xdg(struct mako_state *state) { return sd_bus_add_object_vtable(state->bus, &state->xdg_slot, service_path, service_interface, service_vtable, state); } void notify_notification_closed(struct mako_notification *notif, enum mako_notification_close_reason reason) { struct mako_state *state = notif->state; sd_bus_emit_signal(state->bus, service_path, service_interface, "NotificationClosed", "uu", notif->id, reason); } void notify_action_invoked(struct mako_action *action) { if (!action->notification->style.actions) { // Actions are disabled for this notification, bail. return; } struct mako_state *state = action->notification->state; sd_bus_emit_signal(state->bus, service_path, service_interface, "ActionInvoked", "us", action->notification->id, action->key); } mako-notifier-1.4/event-loop.c000066400000000000000000000137241351432077500163740ustar00rootroot00000000000000#define _POSIX_C_SOURCE 199309L #include #include #include #include #include #include #include #include #include #include "event-loop.h" static int init_signalfd() { sigset_t mask; int sfd; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGQUIT); if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) { fprintf(stderr, "sigprocmask: %s", strerror(errno)); return -1; } if ((sfd = signalfd(-1, &mask, SFD_NONBLOCK)) == -1) { fprintf(stderr, "signalfd: %s", strerror(errno)); return -1; } return sfd; } bool init_event_loop(struct mako_event_loop *loop, sd_bus *bus, struct wl_display *display) { if ((loop->sfd = init_signalfd()) == -1) { return false; } loop->fds[MAKO_EVENT_SIGNAL] = (struct pollfd){ .fd = loop->sfd, .events = POLLIN, }; loop->fds[MAKO_EVENT_DBUS] = (struct pollfd){ .fd = sd_bus_get_fd(bus), .events = POLLIN, }; loop->fds[MAKO_EVENT_WAYLAND] = (struct pollfd){ .fd = wl_display_get_fd(display), .events = POLLIN, }; loop->fds[MAKO_EVENT_TIMER] = (struct pollfd){ .fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC), .events = POLLIN, }; loop->bus = bus; loop->display = display; wl_list_init(&loop->timers); return true; } void finish_event_loop(struct mako_event_loop *loop) { close(loop->fds[MAKO_EVENT_TIMER].fd); loop->fds[MAKO_EVENT_TIMER].fd = -1; struct mako_timer *timer, *tmp; wl_list_for_each_safe(timer, tmp, &loop->timers, link) { destroy_timer(timer); } } static void timespec_add(struct timespec *t, int delta_ms) { static const long ms = 1000000, s = 1000000000; int delta_ms_low = delta_ms % 1000; int delta_s_high = delta_ms / 1000; t->tv_sec += delta_s_high; t->tv_nsec += (long)delta_ms_low * ms; if (t->tv_nsec >= s) { t->tv_nsec -= s; ++t->tv_sec; } } static bool timespec_less(struct timespec *t1, struct timespec *t2) { if (t1->tv_sec != t2->tv_sec) { return t1->tv_sec < t2->tv_sec; } return t1->tv_nsec < t2->tv_nsec; } static void update_event_loop_timer(struct mako_event_loop *loop) { int timer_fd = loop->fds[MAKO_EVENT_TIMER].fd; if (timer_fd < 0) { return; } bool updated = false; struct mako_timer *timer; wl_list_for_each(timer, &loop->timers, link) { if (loop->next_timer == NULL || timespec_less(&timer->at, &loop->next_timer->at)) { loop->next_timer = timer; updated = true; } } if (updated) { struct itimerspec delay = { .it_value = loop->next_timer->at }; errno = 0; int ret = timerfd_settime(timer_fd, TFD_TIMER_ABSTIME, &delay, NULL); if (ret < 0) { fprintf(stderr, "failed to timerfd_settime(): %s\n", strerror(errno)); } } } struct mako_timer *add_event_loop_timer(struct mako_event_loop *loop, int delay_ms, mako_event_loop_timer_func_t func, void *data) { struct mako_timer *timer = calloc(1, sizeof(struct mako_timer)); if (timer == NULL) { fprintf(stderr, "allocation failed\n"); return NULL; } timer->event_loop = loop; timer->func = func; timer->user_data = data; wl_list_insert(&loop->timers, &timer->link); clock_gettime(CLOCK_MONOTONIC, &timer->at); timespec_add(&timer->at, delay_ms); update_event_loop_timer(loop); return timer; } void destroy_timer(struct mako_timer *timer) { if (timer == NULL) { return; } struct mako_event_loop *loop = timer->event_loop; if (loop->next_timer == timer) { loop->next_timer = NULL; } wl_list_remove(&timer->link); free(timer); update_event_loop_timer(loop); } static void handle_event_loop_timer(struct mako_event_loop *loop) { int timer_fd = loop->fds[MAKO_EVENT_TIMER].fd; uint64_t expirations; ssize_t n = read(timer_fd, &expirations, sizeof(expirations)); if (n < 0) { fprintf(stderr, "failed to read from timer FD\n"); return; } struct mako_timer *timer = loop->next_timer; if (timer == NULL) { return; } mako_event_loop_timer_func_t func = timer->func; void *user_data = timer->user_data; destroy_timer(timer); func(user_data); } int run_event_loop(struct mako_event_loop *loop) { loop->running = true; int ret = 0; while (loop->running) { errno = 0; ret = poll(loop->fds, MAKO_EVENT_COUNT, -1); if (!loop->running) { ret = 0; break; } if (ret < 0) { fprintf(stderr, "failed to poll(): %s\n", strerror(errno)); break; } for (size_t i = 0; i < MAKO_EVENT_COUNT; ++i) { if (loop->fds[i].revents & POLLHUP) { loop->running = false; break; } if (loop->fds[i].revents & POLLERR) { fprintf(stderr, "failed to poll() socket %zu\n", i); ret = -1; break; } } if (!loop->running || ret < 0) { break; } if (loop->fds[MAKO_EVENT_SIGNAL].revents & POLLIN) { break; } if (loop->fds[MAKO_EVENT_DBUS].revents & POLLIN) { do { ret = sd_bus_process(loop->bus, NULL); } while (ret > 0); if (ret < 0) { fprintf(stderr, "failed to process D-Bus: %s\n", strerror(-ret)); break; } } if (loop->fds[MAKO_EVENT_DBUS].revents & POLLOUT) { ret = sd_bus_flush(loop->bus); if (ret < 0) { fprintf(stderr, "failed to flush D-Bus: %s\n", strerror(-ret)); break; } } if (loop->fds[MAKO_EVENT_WAYLAND].revents & POLLIN) { ret = wl_display_dispatch(loop->display); if (ret < 0) { fprintf(stderr, "failed to read Wayland events\n"); break; } } if (loop->fds[MAKO_EVENT_WAYLAND].revents & POLLOUT) { ret = wl_display_flush(loop->display); if (ret < 0) { fprintf(stderr, "failed to flush Wayland events\n"); break; } } if (loop->fds[MAKO_EVENT_TIMER].revents & POLLIN) { handle_event_loop_timer(loop); } // Wayland requests can be generated while handling non-Wayland events. // We need to flush these. do { ret = wl_display_dispatch_pending(loop->display); wl_display_flush(loop->display); } while (ret > 0); if (ret < 0) { fprintf(stderr, "failed to dispatch pending Wayland events\n"); break; } // Same for D-Bus. sd_bus_flush(loop->bus); } return ret; } mako-notifier-1.4/fr.emersion.mako.service.in000066400000000000000000000001061351432077500212720ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.Notifications Exec=@bindir@/mako mako-notifier-1.4/icon.c000066400000000000000000000173731351432077500152400ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include "mako.h" #include "icon.h" #include "string-util.h" #include "wayland.h" #ifdef HAVE_ICONS #include #include "cairo-pixbuf.h" static GdkPixbuf *load_image(const char *path) { if (strlen(path) == 0) { return NULL; } GError *err = NULL; GdkPixbuf *pixbuf = gdk_pixbuf_new_from_file(path, &err); if (!pixbuf) { fprintf(stderr, "Failed to load icon (%s)\n", err->message); g_error_free(err); return NULL; } return pixbuf; } static GdkPixbuf *load_image_data(struct mako_image_data *image_data) { GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(image_data->data, GDK_COLORSPACE_RGB, image_data->has_alpha, image_data->bits_per_sample, image_data->width, image_data->height, image_data->rowstride, NULL, NULL); if (!pixbuf) { fprintf(stderr, "Failed to load icon\n"); return NULL; } return pixbuf; } static double fit_to_square(int width, int height, int square_size) { double longest = width > height ? width : height; return longest > square_size ? square_size/longest : 1.0; } static char hex_val(char digit) { assert(isxdigit(digit)); if (digit >= 'a') { return digit - 'a' + 10; } else if (digit >= 'A') { return digit - 'A' + 10; } else { return digit - '0'; } } static void url_decode(char *dst, const char *src) { while (src[0]) { if (src[0] == '%' && isxdigit(src[1]) && isxdigit(src[2])) { dst[0] = 16*hex_val(src[1]) + hex_val(src[2]); dst++; src += 3; } else { dst[0] = src[0]; dst++; src++; } } dst[0] = '\0'; } // Attempt to find a full path for a notification's icon_name, which may be: // - An absolute path, which will simply be returned (as a new string) // - A file:// URI, which will be converted to an absolute path // - A Freedesktop icon name, which will be resolved within the configured // `icon-path` using something that looks vaguely like the algorithm defined // in the icon theme spec (https://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html) // // Returns the resolved path, or NULL if it was unable to find an icon. The // return value must be freed by the caller. static char *resolve_icon(struct mako_notification *notif) { char *icon_name = notif->app_icon; if (icon_name[0] == '\0') { return NULL; } if (icon_name[0] == '/') { return strdup(icon_name); } if (strstr(icon_name, "file://") == icon_name) { // Chop off the scheme and URL decode char *icon_path = malloc(strlen(icon_name) + 1 - strlen("file://")); if (icon_path == NULL) { return icon_path; } url_decode(icon_path, icon_name + strlen("file://")); return icon_path; } // Determine the largest scale factor of any attached output. int32_t max_scale = 1; struct mako_output *output = NULL; wl_list_for_each(output, ¬if->state->outputs, link) { if (output->scale > max_scale) { max_scale = output->scale; } } static const char fallback[] = "%s:/usr/share/icons/hicolor"; char *search = mako_asprintf(fallback, notif->style.icon_path); char *saveptr = NULL; char *theme_path = strtok_r(search, ":", &saveptr); // Match all icon files underneath of the theme_path followed by any icon // size and category subdirectories. This pattern assumes that all the // files in the icon path are valid icon types. static const char pattern_fmt[] = "%s/*/*/%s.*"; char *icon_path = NULL; int32_t last_icon_size = 0; while (theme_path) { if (strlen(theme_path) == 0) { continue; } glob_t icon_glob = {0}; char *pattern = mako_asprintf(pattern_fmt, theme_path, icon_name); // Disable sorting because we're going to do our own anyway. int found = glob(pattern, GLOB_NOSORT, NULL, &icon_glob); size_t found_count = 0; if (found == 0) { // The value of gl_pathc isn't guaranteed to be usable if glob // returns non-zero. found_count = icon_glob.gl_pathc; } for (size_t i = 0; i < found_count; ++i) { char *relative_path = icon_glob.gl_pathv[i]; // Find the end of the current search path and walk to the next // path component. Hopefully this will be the icon resolution // subdirectory. relative_path += strlen(theme_path); while (relative_path[0] == '/') { ++relative_path; } errno = 0; int32_t icon_size = strtol(relative_path, NULL, 10); if (errno || icon_size == 0) { // Try second level subdirectory if failed. errno = 0; while (relative_path[0] != '/') { ++relative_path; } ++relative_path; icon_size = strtol(relative_path, NULL, 10); if (errno || icon_size == 0) { continue; } } int32_t icon_scale = 1; char *scale_str = strchr(relative_path, '@'); if (scale_str != NULL) { icon_scale = strtol(scale_str + 1, NULL, 10); } if (icon_size == notif->style.max_icon_size && icon_scale == max_scale) { // If we find an exact match, we're done. free(icon_path); icon_path = strdup(icon_glob.gl_pathv[i]); break; } else if (icon_size < notif->style.max_icon_size * max_scale && icon_size > last_icon_size) { // Otherwise, if this icon is small enough to fit but bigger // than the last best match, choose it on a provisional basis. // We multiply by max_scale to increase the odds of finding an // icon which looks sharp on the highest-scale output. free(icon_path); icon_path = strdup(icon_glob.gl_pathv[i]); last_icon_size = icon_size; } } free(pattern); globfree(&icon_glob); if (icon_path) { // The spec says that if we find any match whatsoever in a theme, // we should stop there to avoid mixing icons from different // themes even if one is a better size. break; } theme_path = strtok_r(NULL, ":", &saveptr); } if (icon_path == NULL) { // Finally, fall back to looking in /usr/share/pixmaps. These are // unsized icons, which may lead to downscaling, but some apps are // still using it. static const char pixmaps_fmt[] = "/usr/share/pixmaps/%s.*"; char *pattern = mako_asprintf(pixmaps_fmt, icon_name); glob_t icon_glob = {0}; int found = glob(pattern, GLOB_NOSORT, NULL, &icon_glob); if (found == 0 && icon_glob.gl_pathc > 0) { icon_path = strdup(icon_glob.gl_pathv[0]); } free(pattern); globfree(&icon_glob); } free(search); return icon_path; } struct mako_icon *create_icon(struct mako_notification *notif) { GdkPixbuf *image = NULL; if (notif->image_data != NULL) { image = load_image_data(notif->image_data); } if (image == NULL) { char *path = resolve_icon(notif); if (path == NULL) { return NULL; } image = load_image(path); free(path); if (image == NULL) { return NULL; } } int image_width = gdk_pixbuf_get_width(image); int image_height = gdk_pixbuf_get_height(image); struct mako_icon *icon = calloc(1, sizeof(struct mako_icon)); icon->scale = fit_to_square( image_width, image_height, notif->style.max_icon_size); icon->width = image_width * icon->scale; icon->height = image_height * icon->scale; icon->image = create_cairo_surface_from_gdk_pixbuf(image); if (icon->image == NULL) { free(icon); return NULL; } g_object_unref(image); return icon; } #else struct mako_icon *create_icon(struct mako_notification *notif) { return NULL; } #endif void draw_icon(cairo_t *cairo, struct mako_icon *icon, double xpos, double ypos, double scale) { cairo_save(cairo); cairo_scale(cairo, scale*icon->scale, scale*icon->scale); cairo_set_source_surface(cairo, icon->image, xpos/icon->scale, ypos/icon->scale); cairo_paint(cairo); cairo_restore(cairo); } void destroy_icon(struct mako_icon *icon) { if (icon != NULL) { if (icon->image != NULL) { cairo_surface_destroy(icon->image); } free(icon); } } mako-notifier-1.4/include/000077500000000000000000000000001351432077500155545ustar00rootroot00000000000000mako-notifier-1.4/include/cairo-pixbuf.h000066400000000000000000000004111351432077500203110ustar00rootroot00000000000000#ifndef MAKO_CAIRO_PIXBUF_H #define MAKO_CAIRO_PIXBUF_H #ifndef HAVE_ICONS #error "gdk_pixbuf is required" #endif #include #include cairo_surface_t *create_cairo_surface_from_gdk_pixbuf(const GdkPixbuf *pixbuf); #endif mako-notifier-1.4/include/config.h000066400000000000000000000047261351432077500172030ustar00rootroot00000000000000#ifndef MAKO_CONFIG_H #define MAKO_CONFIG_H #include #include #include #include "types.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" enum mako_binding { MAKO_BINDING_NONE, MAKO_BINDING_DISMISS, MAKO_BINDING_DISMISS_ALL, MAKO_BINDING_INVOKE_DEFAULT_ACTION, }; enum mako_sort_criteria { MAKO_SORT_CRITERIA_TIME = 1, MAKO_SORT_CRITERIA_URGENCY = 2, }; // Represents which fields in the style were specified in this style. All // fields in the mako_style structure should have a counterpart here. Inline // structs are also mirrored. struct mako_style_spec { bool width, height, margin, padding, border_size, border_radius, font, markup, format, actions, default_timeout, ignore_timeout, icons, max_icon_size, icon_path, group_criteria_spec, invisible; struct { bool background, text, border, progress; } colors; }; struct mako_style { struct mako_style_spec spec; int32_t width; int32_t height; struct mako_directional margin; struct mako_directional padding; int32_t border_size; int32_t border_radius; bool icons; int32_t max_icon_size; char *icon_path; char *font; bool markup; char *format; bool actions; int default_timeout; // in ms bool ignore_timeout; struct { uint32_t background; uint32_t text; uint32_t border; struct mako_color progress; } colors; struct mako_criteria_spec group_criteria_spec; bool invisible; // Skipped during render, doesn't count toward max_visible }; struct mako_config { struct wl_list criteria; // mako_criteria::link int32_t max_visible; char *output; enum zwlr_layer_shell_v1_layer layer; uint32_t anchor; uint32_t sort_criteria; //enum mako_sort_criteria uint32_t sort_asc; struct mako_style hidden_style; struct mako_style superstyle; struct { enum mako_binding left, right, middle; } button_bindings; enum mako_binding touch; }; 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); bool apply_superset_style( struct mako_style *target, struct mako_config *config); int parse_config_arguments(struct mako_config *config, int argc, char **argv); int load_config_file(struct mako_config *config); int reload_config(struct mako_config *config, int argc, char **argv); #endif mako-notifier-1.4/include/criteria.h000066400000000000000000000026521351432077500175340ustar00rootroot00000000000000#ifndef MAKO_CRITERIA_H #define MAKO_CRITERIA_H #include #include #include #include "config.h" #include "types.h" struct mako_config; struct mako_notification; struct mako_criteria { struct mako_criteria_spec spec; struct wl_list link; // mako_config::criteria char *raw_string; // For debugging // 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 bool expiring; // Whether mako_notification.requested_timeout is non-zero enum mako_notification_urgency urgency; char *category; char *desktop_entry; char *summary; char *body; int group_index; bool grouped; // Whether group_index is non-zero }; 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_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); struct mako_criteria *create_criteria_from_notification( struct mako_notification *notif, struct mako_criteria_spec *spec); #endif mako-notifier-1.4/include/dbus.h000066400000000000000000000011571351432077500166660ustar00rootroot00000000000000#ifndef MAKO_DBUS_H #define MAKO_DBUS_H #include #ifdef HAVE_SYSTEMD #include #elif HAVE_ELOGIND #include #endif struct mako_state; struct mako_notification; struct mako_action; enum mako_notification_close_reason; bool init_dbus(struct mako_state *state); void finish_dbus(struct mako_state *state); void notify_notification_closed(struct mako_notification *notif, enum mako_notification_close_reason reason); void notify_action_invoked(struct mako_action *action); int init_dbus_xdg(struct mako_state *state); int init_dbus_mako(struct mako_state *state); #endif mako-notifier-1.4/include/enum.h000066400000000000000000000005451351432077500166750ustar00rootroot00000000000000#ifndef MAKO_ENUM_H_ #define MAKO_ENUM_H_ // 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, MAKO_PARSE_STATE_FORMAT = 4, }; #endif mako-notifier-1.4/include/event-loop.h000066400000000000000000000022771351432077500200250ustar00rootroot00000000000000#ifndef MAKO_EVENT_LOOP_H #define MAKO_EVENT_LOOP_H #include #include #include #include #ifdef HAVE_SYSTEMD #include #elif HAVE_ELOGIND #include #endif enum mako_event { MAKO_EVENT_DBUS, MAKO_EVENT_WAYLAND, MAKO_EVENT_TIMER, MAKO_EVENT_SIGNAL, MAKO_EVENT_COUNT, // keep last }; struct mako_event_loop { struct pollfd fds[MAKO_EVENT_COUNT]; sd_bus *bus; struct wl_display *display; int sfd; bool running; struct wl_list timers; // mako_timer::link struct mako_timer *next_timer; }; typedef void (*mako_event_loop_timer_func_t)(void *data); struct mako_timer { struct mako_event_loop *event_loop; mako_event_loop_timer_func_t func; void *user_data; struct timespec at; struct wl_list link; // mako_event_loop::timers }; bool init_event_loop(struct mako_event_loop *loop, sd_bus *bus, struct wl_display *display); void finish_event_loop(struct mako_event_loop *loop); int run_event_loop(struct mako_event_loop *loop); struct mako_timer *add_event_loop_timer(struct mako_event_loop *loop, int delay_ms, mako_event_loop_timer_func_t func, void *data); void destroy_timer(struct mako_timer *timer); #endif mako-notifier-1.4/include/icon.h000066400000000000000000000010641351432077500166560ustar00rootroot00000000000000#ifndef MAKO_ICON_H #define MAKO_ICON_H #include #include "notification.h" struct mako_icon { double width; double height; double scale; cairo_surface_t *image; }; struct mako_image_data { int32_t width; int32_t height; int32_t rowstride; uint32_t has_alpha; int32_t bits_per_sample; int32_t channels; uint8_t *data; }; struct mako_icon *create_icon(struct mako_notification *notif); void destroy_icon(struct mako_icon *icon); void draw_icon(cairo_t *cairo, struct mako_icon *icon, double xpos, double ypos, double scale); #endif mako-notifier-1.4/include/mako.h000066400000000000000000000024151351432077500166560ustar00rootroot00000000000000#ifndef MAKO_H #define MAKO_H #include #include #ifdef HAVE_SYSTEMD #include #elif HAVE_ELOGIND #include #endif #include "config.h" #include "event-loop.h" #include "pool-buffer.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "xdg-output-unstable-v1-client-protocol.h" struct mako_state { struct mako_config config; struct mako_event_loop event_loop; sd_bus *bus; sd_bus_slot *xdg_slot, *mako_slot; struct wl_display *display; struct wl_registry *registry; struct wl_compositor *compositor; struct wl_shm *shm; struct zwlr_layer_shell_v1 *layer_shell; struct zxdg_output_manager_v1 *xdg_output_manager; struct wl_list outputs; // mako_output::link struct wl_list seats; // mako_seat::link struct wl_surface *surface; struct mako_output *surface_output; struct zwlr_layer_surface_v1 *layer_surface; struct mako_output *layer_surface_output; bool configured; bool frame_pending; // Have we requested a frame callback? bool dirty; // Do we need to redraw? int32_t scale; int32_t width, height; struct pool_buffer buffers[2]; struct pool_buffer *current_buffer; uint32_t last_id; struct wl_list notifications; // mako_notification::link int argc; char **argv; }; #endif mako-notifier-1.4/include/notification.h000066400000000000000000000053101351432077500204120ustar00rootroot00000000000000#ifndef MAKO_NOTIFICATION_H #define MAKO_NOTIFICATION_H #include #include #include #include "config.h" #include "types.h" struct mako_state; struct mako_timer; struct mako_criteria; struct mako_icon; struct mako_hotspot { int32_t x, y; int32_t width, height; }; struct mako_notification { struct mako_state *state; struct wl_list link; // mako_state::notifications struct mako_style style; struct mako_icon *icon; uint32_t id; int group_index; int group_count; char *app_name; char *app_icon; char *summary; char *body; int32_t requested_timeout; struct wl_list actions; // mako_action::link enum mako_notification_urgency urgency; char *category; char *desktop_entry; int32_t progress; struct mako_image_data *image_data; struct mako_hotspot hotspot; struct mako_timer *timer; }; struct mako_action { struct mako_notification *notification; struct wl_list link; // mako_notification::actions char *key; char *title; }; enum mako_notification_close_reason { MAKO_NOTIFICATION_CLOSE_EXPIRED = 1, MAKO_NOTIFICATION_CLOSE_DISMISSED = 2, MAKO_NOTIFICATION_CLOSE_REQUEST = 3, MAKO_NOTIFICATION_CLOSE_UNKNOWN = 4, }; // Tiny struct to be the data type for format_hidden_text. struct mako_hidden_format_data { size_t hidden; size_t count; }; #define DEFAULT_ACTION_KEY "default" typedef char *(*mako_format_func_t)(char variable, bool *markup, void *data); bool hotspot_at(struct mako_hotspot *hotspot, int32_t x, int32_t y); void reset_notification(struct mako_notification *notif); struct mako_notification *create_notification(struct mako_state *state); void destroy_notification(struct mako_notification *notif); void close_notification(struct mako_notification *notif, enum mako_notification_close_reason reason); void close_all_notifications(struct mako_state *state, enum mako_notification_close_reason reason); char *format_hidden_text(char variable, bool *markup, void *data); char *format_notif_text(char variable, bool *markup, void *data); size_t format_text(const char *format, char *buf, mako_format_func_t func, void *data); struct mako_notification *get_notification(struct mako_state *state, uint32_t id); size_t format_notification(struct mako_notification *notif, const char *format, char *buf); void notification_handle_button(struct mako_notification *notif, uint32_t button, enum wl_pointer_button_state state); void notification_handle_touch(struct mako_notification *notif); void notification_execute_binding(struct mako_notification *notif, enum mako_binding binding); void insert_notification(struct mako_state *state, struct mako_notification *notif); int group_notifications(struct mako_state *state, struct mako_criteria *criteria); #endif mako-notifier-1.4/include/pool-buffer.h000066400000000000000000000010401351432077500201400ustar00rootroot00000000000000#ifndef MAKO_POOL_BUFFER_H #define MAKO_POOL_BUFFER_H #include #include #include #include #include struct pool_buffer { struct wl_buffer *buffer; cairo_surface_t *surface; cairo_t *cairo; PangoContext *pango; uint32_t width, height; void *data; size_t size; bool busy; }; struct pool_buffer *get_next_buffer(struct wl_shm *shm, struct pool_buffer pool[static 2], uint32_t width, uint32_t height); void finish_buffer(struct pool_buffer *buffer); #endif mako-notifier-1.4/include/render.h000066400000000000000000000002261351432077500172040ustar00rootroot00000000000000#ifndef MAKO_RENDER_H #define MAKO_RENDER_H struct mako_state; int render(struct mako_state *state, struct pool_buffer *buffer, int scale); #endif mako-notifier-1.4/include/string-util.h000066400000000000000000000001401351432077500202010ustar00rootroot00000000000000#ifndef MAKO_STRING_H #define MAKO_STRING_H char *mako_asprintf(const char *fmt, ...); #endif mako-notifier-1.4/include/types.h000066400000000000000000000032711351432077500170740ustar00rootroot00000000000000#ifndef MAKO_TYPES_H #define MAKO_TYPES_H #include #include #include struct mako_color { uint32_t value; cairo_operator_t operator; }; bool parse_boolean(const char *string, bool *out); bool parse_int(const char *string, int *out); bool parse_color(const char *string, uint32_t *out); bool parse_mako_color(const char *string, struct mako_color *out); enum mako_notification_urgency { MAKO_NOTIFICATION_URGENCY_LOW = 0, MAKO_NOTIFICATION_URGENCY_NORMAL = 1, MAKO_NOTIFICATION_URGENCY_HIGH = 2, MAKO_NOTIFICATION_URGENCY_UNKNOWN = -1, }; bool parse_urgency(const char *string, enum mako_notification_urgency *out); struct mako_directional { int32_t top; int32_t right; int32_t bottom; int32_t left; }; bool parse_directional(const char *string, struct mako_directional *out); // Criteria specifications are used for two things. // Primarily, they keep track of whether or not each field was part of the a // criteria specification, so that, for example, "not actionable" can be // distinguished from "don't care". // Additionally, they are used to store the set of criteria that must match for // notifications to group with each other. struct mako_criteria_spec { bool app_name; bool app_icon; bool actionable; bool expiring; bool urgency; bool category; bool desktop_entry; bool summary; bool body; bool group_index; bool grouped; bool none; // Special criteria that never matches, used for grouping }; bool parse_criteria_spec(const char *string, struct mako_criteria_spec *out); // List of specifier characters that can appear in a format string. extern const char VALID_FORMAT_SPECIFIERS[]; bool parse_format(const char *string, char **out); #endif mako-notifier-1.4/include/wayland.h000066400000000000000000000014101351432077500173600ustar00rootroot00000000000000#ifndef MAKO_WAYLAND_H #define MAKO_WAYLAND_H #include #include struct mako_state; struct mako_output { struct mako_state *state; uint32_t global_name; struct wl_output *wl_output; struct zxdg_output_v1 *xdg_output; struct wl_list link; // mako_state::outputs char *name; enum wl_output_subpixel subpixel; int32_t scale; }; struct mako_seat { struct mako_state *state; struct wl_seat *wl_seat; struct wl_list link; // mako_state::seats struct { struct wl_pointer *wl_pointer; int32_t x, y; } pointer; struct { struct wl_touch *wl_touch; int32_t x, y; } touch; }; bool init_wayland(struct mako_state *state); void finish_wayland(struct mako_state *state); void set_dirty(struct mako_state *state); #endif mako-notifier-1.4/main.c000066400000000000000000000076031351432077500152270ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include "config.h" #include "dbus.h" #include "mako.h" #include "notification.h" #include "render.h" #include "wayland.h" static const char usage[] = "Usage: mako [options...]\n" "\n" " -h, --help Show help message and quit.\n" " --font Font family and size.\n" " --background-color Background color.\n" " --text-color Text color.\n" " --width Notification width.\n" " --height Max notification height.\n" " --margin [,...] Margin values, comma separated.\n" " Up to four values, with the same\n" " meaning as in CSS.\n" " --padding [,...] Padding values, comma separated.\n" " Up to four values, with the same\n" " meaning as in CSS.\n" " --border-size Border size.\n" " --border-color Border color.\n" " --border-radius Corner radius\n" " --progress-color Progress indicator color.\n" " --icons <0|1> Show icons in notifications.\n" " --icon-path [:...] Icon search path, colon delimited.\n" " --max-icon-size Set max size of icons.\n" " --markup <0|1> Enable/disable markup.\n" " --actions <0|1> Enable/disable application action\n" " execution.\n" " --format Format string.\n" " --hidden-format Format string.\n" " --max-visible Max number of visible notifications.\n" " --sort Sorts incoming notifications by time\n" " and/or priority in ascending(+) or\n" " descending(-) order.\n" " --default-timeout Default timeout in milliseconds.\n" " --ignore-timeout <0|1> Enable/disable notification timeout.\n" " --output Show notifications on this output.\n" " --layer Arrange notifications at this layer.\n" " --anchor Position on output to put notifications.\n" "\n" "Colors can be specified with the format #RRGGBB or #RRGGBBAA.\n"; static bool init(struct mako_state *state) { if (!init_dbus(state)) { return false; } if (!init_wayland(state)) { finish_dbus(state); return false; } if (!init_event_loop(&state->event_loop, state->bus, state->display)) { finish_dbus(state); finish_wayland(state); return false; } wl_list_init(&state->notifications); return true; } static void finish(struct mako_state *state) { struct mako_notification *notif, *tmp; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { destroy_notification(notif); } finish_event_loop(&state->event_loop); finish_wayland(state); finish_dbus(state); } static struct mako_event_loop *event_loop = NULL; int main(int argc, char *argv[]) { struct mako_state state = {0}; state.argc = argc; state.argv = argv; // This is a bit wasteful, but easier than special-casing the reload. init_default_config(&state.config); int ret = reload_config(&state.config, argc, argv); if (ret < 0) { return EXIT_FAILURE; } else if (ret > 0) { printf("%s", usage); return EXIT_SUCCESS; } if (!init(&state)) { finish_config(&state.config); return EXIT_FAILURE; } event_loop = &state.event_loop; ret = run_event_loop(&state.event_loop); finish(&state); finish_config(&state.config); return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS; } mako-notifier-1.4/mako.1.scd000066400000000000000000000223731351432077500157210ustar00rootroot00000000000000mako(1) # NAME mako - notification daemon for Wayland # SYNOPSIS *mako* [options...] # DESCRIPTION mako is a graphical notification daemon for Wayland compositors which support the layer-shell protocol. Notifications received over dbus are displayed until dismissed with a click or via *makoctl*(1). # OPTIONS *-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: "" *--layer* _layer_ Arrange mako at the specified layer, relative to normal windows. Supported values are _background_, _bottom_, _top_, and _overlay_. Using _overlay_ will cause notifications to be displayed above fullscreen windows, though this may also occur at _top_ depending on your compositor. Default: top *--anchor* _position_ Show notifications at the specified position on the output. Supported values are _top-right_, _top-center_, _top-left_, _bottom-right_, _bottom-center_, _bottom-left_, and _center_. Default: top-right # STYLE OPTIONS *--font* _font_ Set font to _font_, in Pango format. Default: monospace 10 *--background-color* _color_ Set background color to _color_. See *COLORS* for more information. Default: #285577FF *--text-color* _color_ Set text color to _color_. See *COLORS* for more information. Default: #FFFFFFFF *--width* _px_ Set width of notification popups. Default: 300 *--height* _px_ Set maximium height of notification popups. Notifications whose text takes up less space are shrunk to fit. Default: 100 *--margin* _directional_ Set margin of each edge to the size specified by _directional_. See *DIRECTIONAL VALUES* for more information. Default: 10 *--padding* _directional_ Set padding on each side to the size specified by _directional_. See *DIRECTIONAL VALUES* for more information. Default: 5 *--border-size* _px_ Set popup border size to _px_ pixels. Default: 1 *--border-color* _color_ Set popup border color to _color_. See *COLORS* for more information. Default: #4C7899FF *--border-radius* _px_ Set popup corner radius to _px_ pixels. Default: 0 *--progress-color* [over|source] _color_ Set popup progress indicator color to _color_. See *COLOR* for more information. To draw the progress indicator on top of the background color, use the *over* attribute. To replace the background color, use the *source* attribute (this can be useful when the notification is semi-transparent). Default: over #5588AAFF *--icons* 0|1 Show icons in notifications. Default: 1 *--max-icon-size* _px_ Set maximum icon size to _px_ pixels. Default: 64 *--icon-path* _path_\[:_path_...\] Paths to search for icons when a notification specifies a name instead of a full path. Colon-delimited. This approximates the search algorithm used by the XDG Icon Theme Specification, but does not support any of the theme metadata. Therefore, if you want to search parent themes, you'll need to add them to the path manually. /usr/share/icons/hicolor and /usr/share/pixmaps are always searched. Default: "" *--markup* 0|1 If 1, enable Pango markup. If 0, disable Pango markup. If enabled, Pango markup will be interpreted in your format specifier and in the body of notifications. Default: 1 *--actions* 0|1 Applications may request an action to be associated with activating a notification. Disabling this will cause mako to ignore these requests. Default: 1 *--format* _format_ Set notification format string to _format_. See *FORMAT SPECIFIERS* for more information. To change this for grouped notifications, set it within a _grouped_ criteria. Default: %s\\n%b Default when grouped: (%g) %s\\n%b *--default-timeout* _timeout_ Set the default timeout to _timeout_ in milliseconds. To disable the timeout, set it to zero. Default: 0 *--ignore-timeout* 0|1 If set, mako will ignore the expire timeout sent by notifications and use the one provided by _default-timeout_ instead. Default: 0 *--group-by* _field[,field,...]_ A comma-separated list of criteria fields that will be compared to other visible notifications to determine if this one should form a group with them. All listed criteria must be exactly equal for two notifications to group. Default: none # CRITERIA-ONLY STYLE OPTIONS Some style options are not useful in the global context and therefore have no associated command-line option. *invisible* 0|1 Whether this notification should be invisible even if it is above the _max-visible_ cutoff. This is used primarily for hiding members of groups. If you want to make more than the first group member visible, turn this option off within a _group-index_ criteria. Default: 0 # CONFIG FILE The config file is located at *~/.config/mako/config* or at *$XDG\_CONFIG\_HOME/mako/config*. Each line of the form: key=value Is equivalent to passing *--key=value* to mako from the command line. Note that any quotes used within your shell are unnecessary and also invalid in the config file. Empty lines and lines that begin with # are ignored. # 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 criteria: - _app-name_ (string) - _app-icon_ (string) - _summary_ (string) - An exact match on the summary of the notification. - _urgency_ (one of "low", "normal", "high") - _category_ (string) - _desktop-entry_ (string) - _actionable_ (boolean) - _expiring_ (boolean) - _grouped_ (boolean) - Whether the notification is grouped with any others (its group-index is not -1). - _group-index_ (int) - The notification's index within its group, or -1 if it is not grouped. - _hidden_ (boolean) - _hidden_ is special, it defines the style for the placeholder shown when the number of notifications or groups 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 backslash 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\] \[actionable=false\] \[actionable=0\] \[!actionable\] There are three criteria always present at the front of the list: - An empty criteria which matches all notifications and contains the defaults for all style options, overwritten with any configured in the global section. - \[grouped\], which sets the default *format* for grouped notifications and sets them *invisible*. - \[group-index=0\], which makes the first member of each group visible again. These options can be overridden by simply defining the criteria yourself and overriding them. # COLORS Colors can be specified as _#RRGGBB_ or _#RRGGBBAA_. # DIRECTIONAL VALUES Some options set values that affect all four edges of a notification. These options can be specified in several different ways, depending on how much control over each edge is desired: - A single value will apply to all four edges. - Two values will set vertical and horizontal edges separately. - Three will set top, horizontal, and bottom edges separately. - Four will give each edge a separate value. When specifying multiple values, they should be comma-separated. For example, this would set the top margin to 10, left and right to 20, and bottom to five: ``` --margin 10,20,5 ``` # FORMAT SPECIFIERS Format specification works similarly to *printf*(3), but with a different set of specifiers. *%%* Literal "%" *\\\\* Literal "\\" *\\n* New Line ## For notifications *%a* Application name *%s* Notification summary *%b* Notification body *%g* Number of notifications in the current group ## For the hidden notifications placeholder *%h* Number of hidden notifications *%t* Total number of notifications # AUTHORS Maintained by Simon Ser , who is assisted by other open-source contributors. For more information about mako development, see https://github.com/emersion/mako. # SEE ALSO *makoctl*(1) mako-notifier-1.4/makoctl000077500000000000000000000024351351432077500155150ustar00rootroot00000000000000#!/bin/sh -eu usage() { echo "Usage: makoctl [options...]" echo "" echo "Commands:" echo " dismiss [-a|--all] Dismiss the last or all notifications" echo " invoke [-n id] [action] Invoke an action on the notification" echo " with the given id, or the last" echo " notification if none is given" echo " list List notifications" echo " reload Reload the configuration file" echo " help Show this help" } call() { busctl -j --user call org.freedesktop.Notifications /fr/emersion/Mako \ fr.emersion.Mako "$@" } if [ $# -eq 0 ] || [ $# -gt 5 ]; then usage exit 1 fi case "$1" in "dismiss") [ $# -lt 2 ] && action="" || action="$2" case "$action" in "-a"|"--all") call DismissAllNotifications ;; "") call DismissLastNotification ;; *) echo "makoctl: unrecognized option '$2'" exit 1 ;; esac ;; "invoke") id=0 if [ $# -gt 1 ] && [ $2 == "-n" ]; then id="$3" shift 2 fi action="default" if [ $# -gt 1 ]; then action="$2" fi call InvokeAction "us" "$id" "$action" ;; "list") call ListNotifications ;; "reload") call Reload ;; "help"|"--help"|"-h") usage ;; *) echo "makoctl: unrecognized command '$1'" exit 1 ;; esac mako-notifier-1.4/makoctl.1.scd000066400000000000000000000014001351432077500164100ustar00rootroot00000000000000makoctl(1) # NAME makoctl - controls the *mako*(1) daemon # SYNOPSIS makoctl [cmd] [options...] # DESCRIPTION Sends IPC commands to the running mako daemon via dbus. # COMMANDS *dismiss* Dismisses the first notification. *Options* *-a, --all* Dismiss all notifications. *invoke* [action] Invokes an action on the first notification. If _action_ is not specified, invokes the default action. *list* Retrieve a list of current notifications. *reload* Reloads the configuration file. *help, -h, --help* Show help message and quit. # AUTHORS Maintained by Simon Ser , who is assisted by other open-source contributors. For more information about mako development, see https://github.com/emersion/mako. # SEE ALSO *mako*(1) mako-notifier-1.4/meson.build000066400000000000000000000050561351432077500163010ustar00rootroot00000000000000project( 'mako', 'c', version: '1.3.0', license: 'MIT', meson_version: '>=0.47.0', default_options: [ 'c_std=c11', 'warning_level=2', 'werror=true', ], ) add_project_arguments('-Wno-unused-parameter', language: 'c') add_project_arguments('-Wno-missing-braces', language: 'c') datadir = get_option('datadir') mako_inc = include_directories('include') cc = meson.get_compiler('c') cairo = dependency('cairo') pango = dependency('pango') pangocairo = dependency('pangocairo') glib = dependency('glib-2.0') gobject = dependency('gobject-2.0') realtime = cc.find_library('rt') wayland_client = dependency('wayland-client') wayland_protos = dependency('wayland-protocols', version: '>=1.14') logind = dependency('libsystemd', required: false) if logind.found() add_project_arguments('-DHAVE_SYSTEMD=1', language: 'c') else logind = dependency('libelogind') add_project_arguments('-DHAVE_ELOGIND=1', language: 'c') endif gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('icons')) if gdk_pixbuf.found() add_global_arguments('-DHAVE_ICONS=1', language: 'c') endif subdir('contrib/apparmor') subdir('contrib/completions') subdir('protocol') src_files = [ 'config.c', 'event-loop.c', 'dbus/dbus.c', 'dbus/mako.c', 'dbus/xdg.c', 'main.c', 'notification.c', 'pool-buffer.c', 'render.c', 'wayland.c', 'criteria.c', 'types.c', 'icon.c', 'string-util.c', ] if gdk_pixbuf.found() src_files += 'cairo-pixbuf.c' endif executable( 'mako', files(src_files), dependencies: [ cairo, client_protos, gdk_pixbuf, logind, pango, pangocairo, glib, gobject, realtime, wayland_client, ], include_directories: [mako_inc], install: true, ) install_data( 'makoctl', install_dir: get_option('bindir'), install_mode: 'rwxr-xr-x', ) conf_data = configuration_data() conf_data.set('bindir', join_paths(get_option('prefix'), get_option('bindir'))) configure_file( configuration: conf_data, input: 'fr.emersion.mako.service.in', output: '@BASENAME@', install_dir: datadir + '/dbus-1/services', ) scdoc = find_program('scdoc', required: get_option('man-pages')) if scdoc.found() sh = find_program('sh') man_pages = ['mako.1.scd', 'makoctl.1.scd'] mandir = get_option('mandir') foreach src : man_pages topic = src.split('.')[0] section = src.split('.')[1] output = '@0@.@1@'.format(topic, section) custom_target( output, input: src, output: output, command: [ sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.path(), output) ], install: true, install_dir: '@0@/man@1@'.format(mandir, section) ) endforeach endif mako-notifier-1.4/meson_options.txt000066400000000000000000000005651351432077500175740ustar00rootroot00000000000000option('icons', type: 'feature', value: 'auto', description: 'Enable icon support') option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('zsh-completions', type: 'boolean', value: false, description: 'Install zsh completions') option('apparmor', type: 'boolean', value: 'false', description: 'Install AppArmor profile') mako-notifier-1.4/notification.c000066400000000000000000000302311351432077500167620ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #ifdef __linux__ #include #elif __FreeBSD__ #include #endif #include "config.h" #include "criteria.h" #include "dbus.h" #include "event-loop.h" #include "mako.h" #include "notification.h" #include "icon.h" #include "string-util.h" bool hotspot_at(struct mako_hotspot *hotspot, int32_t x, int32_t y) { return x >= hotspot->x && y >= hotspot->y && x < hotspot->x + hotspot->width && y < hotspot->y + hotspot->height; } void reset_notification(struct mako_notification *notif) { struct mako_action *action, *tmp; wl_list_for_each_safe(action, tmp, ¬if->actions, link) { wl_list_remove(&action->link); free(action->key); free(action->title); free(action); } notif->urgency = MAKO_NOTIFICATION_URGENCY_UNKNOWN; notif->progress = -1; destroy_timer(notif->timer); notif->timer = NULL; free(notif->app_name); free(notif->app_icon); free(notif->summary); free(notif->body); free(notif->category); free(notif->desktop_entry); if (notif->image_data != NULL) { free(notif->image_data->data); free(notif->image_data); } notif->app_name = NULL; notif->app_icon = NULL; notif->summary = NULL; notif->body = NULL; notif->category = NULL; notif->desktop_entry = NULL; notif->image_data = NULL; destroy_icon(notif->icon); notif->icon = NULL; } struct mako_notification *create_notification(struct mako_state *state) { struct mako_notification *notif = calloc(1, sizeof(struct mako_notification)); if (notif == NULL) { fprintf(stderr, "allocation failed\n"); return NULL; } notif->state = state; ++state->last_id; notif->id = state->last_id; wl_list_init(¬if->actions); reset_notification(notif); // Start ungrouped. notif->group_index = -1; return notif; } void destroy_notification(struct mako_notification *notif) { wl_list_remove(¬if->link); reset_notification(notif); finish_style(¬if->style); free(notif); } void close_notification(struct mako_notification *notif, enum mako_notification_close_reason reason) { notify_notification_closed(notif, reason); wl_list_remove(¬if->link); // Remove so regrouping works... wl_list_init(¬if->link); // ...but destroy will remove again. struct mako_criteria *notif_criteria = create_criteria_from_notification( notif, ¬if->style.group_criteria_spec); if (notif_criteria) { group_notifications(notif->state, notif_criteria); free(notif_criteria); } destroy_notification(notif); } struct mako_notification *get_notification(struct mako_state *state, uint32_t id) { struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (notif->id == id) { return notif; } } return NULL; } void close_all_notifications(struct mako_state *state, enum mako_notification_close_reason reason) { struct mako_notification *notif, *tmp; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { close_notification(notif, reason); } } static size_t trim_space(char *dst, const char *src) { size_t src_len = strlen(src); const char *start = src; const char *end = src + src_len; while (start != end && isspace(start[0])) { ++start; } while (end != start && isspace(end[-1])) { --end; } size_t trimmed_len = end - start; memmove(dst, start, trimmed_len); dst[trimmed_len] = '\0'; return trimmed_len; } static const char *escape_markup_char(char c) { switch (c) { case '&': return "&"; case '<': return "<"; case '>': return ">"; case '\'': return "'"; case '"': return """; } return NULL; } static size_t escape_markup(const char *s, char *buf) { size_t len = 0; while (s[0] != '\0') { const char *replacement = escape_markup_char(s[0]); if (replacement != NULL) { size_t replacement_len = strlen(replacement); if (buf != NULL) { memcpy(buf + len, replacement, replacement_len); } len += replacement_len; } else { if (buf != NULL) { buf[len] = s[0]; } ++len; } ++s; } if (buf != NULL) { buf[len] = '\0'; } return len; } // Any new format specifiers must also be added to VALID_FORMAT_SPECIFIERS. char *format_hidden_text(char variable, bool *markup, void *data) { struct mako_hidden_format_data *format_data = data; switch (variable) { case 'h': return mako_asprintf("%zu", format_data->hidden); case 't': return mako_asprintf("%zu", format_data->count); } return NULL; } char *format_notif_text(char variable, bool *markup, void *data) { struct mako_notification *notif = data; switch (variable) { case 'a': return strdup(notif->app_name); case 's': return strdup(notif->summary); case 'b': *markup = notif->style.markup; return strdup(notif->body); case 'g': return mako_asprintf("%d", notif->group_count); } return NULL; } size_t format_text(const char *format, char *buf, mako_format_func_t format_func, void *data) { size_t len = 0; const char *last = format; while (1) { char *current = strchr(last, '%'); if (current == NULL || current[1] == '\0') { size_t tail_len = strlen(last); if (buf != NULL) { memcpy(buf + len, last, tail_len + 1); } len += tail_len; break; } size_t chunk_len = current - last; if (buf != NULL) { memcpy(buf + len, last, chunk_len); } len += chunk_len; char *value = NULL; bool markup = false; if (current[1] == '%') { value = strdup("%"); } else { value = format_func(current[1], &markup, data); } if (value == NULL) { value = strdup(""); } size_t value_len; if (!markup || !pango_parse_markup(value, -1, 0, NULL, NULL, NULL, NULL)) { char *escaped = NULL; if (buf != NULL) { escaped = buf + len; } value_len = escape_markup(value, escaped); } else { value_len = strlen(value); if (buf != NULL) { memcpy(buf + len, value, value_len); } } free(value); len += value_len; last = current + 2; } if (buf != NULL) { trim_space(buf, buf); } return len; } static enum mako_binding get_binding(struct mako_config *config, uint32_t button) { switch (button) { case BTN_LEFT: return config->button_bindings.left; case BTN_RIGHT: return config->button_bindings.right; case BTN_MIDDLE: return config->button_bindings.middle; } return MAKO_BINDING_NONE; } void notification_execute_binding(struct mako_notification *notif, enum mako_binding binding) { switch (binding) { case MAKO_BINDING_NONE: break; case MAKO_BINDING_DISMISS: close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED); break; case MAKO_BINDING_DISMISS_ALL: close_all_notifications(notif->state, MAKO_NOTIFICATION_CLOSE_DISMISSED); break; case MAKO_BINDING_INVOKE_DEFAULT_ACTION:; struct mako_action *action; wl_list_for_each(action, ¬if->actions, link) { if (strcmp(action->key, DEFAULT_ACTION_KEY) == 0) { notify_action_invoked(action); break; } } close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED); break; } } void notification_handle_button(struct mako_notification *notif, uint32_t button, enum wl_pointer_button_state state) { if (state != WL_POINTER_BUTTON_STATE_PRESSED) { return; } notification_execute_binding(notif, get_binding(¬if->state->config, button)); } void notification_handle_touch(struct mako_notification *notif) { notification_execute_binding(notif, notif->state->config.touch); } /* * Searches through the notifications list and returns the next position at * which to insert. If no results for the specified urgency are found, * it will return the closest link searching in the direction specifed. * (-1 for lower, 1 or upper). */ static struct wl_list *get_last_notif_by_urgency(struct wl_list *notifications, enum mako_notification_urgency urgency, int direction) { enum mako_notification_urgency current = urgency; if (wl_list_empty(notifications)) { return notifications; } while (current <= MAKO_NOTIFICATION_URGENCY_HIGH && current >= MAKO_NOTIFICATION_URGENCY_UNKNOWN) { struct mako_notification *notif; wl_list_for_each_reverse(notif, notifications, link) { if (notif->urgency == current) { return ¬if->link; } } current += direction; } return notifications; } void insert_notification(struct mako_state *state, struct mako_notification *notif) { struct mako_config *config = &state->config; struct wl_list *insert_node; if (config->sort_criteria == MAKO_SORT_CRITERIA_TIME && !(config->sort_asc & MAKO_SORT_CRITERIA_TIME)) { insert_node = &state->notifications; } else if (config->sort_criteria == MAKO_SORT_CRITERIA_TIME && (config->sort_asc & MAKO_SORT_CRITERIA_TIME)) { insert_node = state->notifications.prev; } else if (config->sort_criteria & MAKO_SORT_CRITERIA_URGENCY) { int direction = (config->sort_asc & MAKO_SORT_CRITERIA_URGENCY) ? -1 : 1; int offset = 0; if (!(config->sort_asc & MAKO_SORT_CRITERIA_TIME)) { offset = direction; } insert_node = get_last_notif_by_urgency(&state->notifications, notif->urgency + offset, direction); } else { insert_node = &state->notifications; } wl_list_insert(insert_node, ¬if->link); } // Iterate through all of the current notifications and group any that share // the same values for all of the criteria fields in `spec`. Returns the number // of notifications in the resulting group, or -1 if something goes wrong // with criteria. int group_notifications(struct mako_state *state, struct mako_criteria *criteria) { struct wl_list matches = {0}; wl_list_init(&matches); // Now we're going to find all of the matching notifications and stick // them in a different list. Removing the first one from the global list // is technically unnecessary, since it will go back in the same place, but // it makes the rest of this logic nicer. struct wl_list *location = NULL; // The place we're going to reinsert them. struct mako_notification *notif = NULL, *tmp = NULL; size_t count = 0; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { if (!match_criteria(criteria, notif)) { continue; } if (!location) { location = notif->link.prev; } wl_list_remove(¬if->link); wl_list_insert(matches.prev, ¬if->link); notif->group_index = count++; } // If count is zero, we don't need to worry about changing anything. The // notification's style has its grouping criteria set to none. if (count == 1) { // If we matched a single notification, it means that it has grouping // criteria set, but didn't have any others to group with. This makes // it ungrouped just as if it had no grouping criteria. If this is a // new notification, its index is already set to -1. However, this also // happens when a notification had been part of a group and all the // others have closed, so we need to set it anyway. // We can't use the current pointer, wl_list_for_each_safe clobbers it. notif = wl_container_of(matches.prev, notif, link); notif->group_index = -1; } // Now we need to rematch criteria for all of the grouped notifications, // in case it changes their styles. We also take this opportunity to record // the total number of notifications in the group, so that it can be used // in the notifications' format. // We can't skip this even if there was only a single match, as we may be // removing the second-to-last notification of a group, and still need to // potentially change style now that the matched one isn't in a group // anymore. wl_list_for_each(notif, &matches, link) { notif->group_count = count; int rematch_count = apply_each_criteria(&state->config.criteria, notif); if (rematch_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 (rematch_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; } } // Place all of the matches back into the list where the first one was // originally. wl_list_insert_list(location, &matches); return count; } mako-notifier-1.4/pool-buffer.c000066400000000000000000000062631351432077500165240ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200112L #include #include #include #include #include #include #include #include #include "pool-buffer.h" static void randname(char *buf) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); long r = ts.tv_nsec; for (int i = 0; i < 6; ++i) { buf[i] = 'A'+(r&15)+(r&16)*2; r >>= 5; } } static int anonymous_shm_open(void) { char name[] = "/mako-XXXXXX"; int retries = 100; do { randname(name + strlen(name) - 6); --retries; // shm_open guarantees that O_CLOEXEC is set int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); if (fd >= 0) { shm_unlink(name); return fd; } } while (retries > 0 && errno == EEXIST); return -1; } static int create_shm_file(off_t size) { int fd = anonymous_shm_open(); if (fd < 0) { return fd; } if (ftruncate(fd, size) < 0) { close(fd); return -1; } return fd; } static void buffer_handle_release(void *data, struct wl_buffer *wl_buffer) { struct pool_buffer *buffer = data; buffer->busy = false; } static const struct wl_buffer_listener buffer_listener = { .release = buffer_handle_release, }; static struct pool_buffer *create_buffer(struct wl_shm *shm, struct pool_buffer *buf, int32_t width, int32_t height) { const enum wl_shm_format wl_fmt = WL_SHM_FORMAT_ARGB8888; const cairo_format_t cairo_fmt = CAIRO_FORMAT_ARGB32; uint32_t stride = cairo_format_stride_for_width(cairo_fmt, width); size_t size = stride * height; void *data = NULL; if (size > 0) { int fd = create_shm_file(size); if (fd == -1) { return NULL; } data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (data == MAP_FAILED) { close(fd); return NULL; } struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); buf->buffer = wl_shm_pool_create_buffer(pool, 0, width, height, stride, wl_fmt); wl_buffer_add_listener(buf->buffer, &buffer_listener, buf); wl_shm_pool_destroy(pool); close(fd); } buf->data = data; buf->size = size; buf->width = width; buf->height = height; buf->surface = cairo_image_surface_create_for_data(data, cairo_fmt, width, height, stride); buf->cairo = cairo_create(buf->surface); buf->pango = pango_cairo_create_context(buf->cairo); return buf; } void finish_buffer(struct pool_buffer *buffer) { if (buffer->buffer) { wl_buffer_destroy(buffer->buffer); } if (buffer->cairo) { cairo_destroy(buffer->cairo); } if (buffer->surface) { cairo_surface_destroy(buffer->surface); } if (buffer->pango) { g_object_unref(buffer->pango); } if (buffer->data) { munmap(buffer->data, buffer->size); } memset(buffer, 0, sizeof(struct pool_buffer)); } struct pool_buffer *get_next_buffer(struct wl_shm *shm, struct pool_buffer pool[static 2], uint32_t width, uint32_t height) { struct pool_buffer *buffer = NULL; for (size_t i = 0; i < 2; ++i) { if (pool[i].busy) { continue; } buffer = &pool[i]; } if (!buffer) { return NULL; } if (buffer->width != width || buffer->height != height) { finish_buffer(buffer); } if (!buffer->buffer) { if (!create_buffer(shm, buffer, width, height)) { return NULL; } } return buffer; } mako-notifier-1.4/protocol/000077500000000000000000000000001351432077500157725ustar00rootroot00000000000000mako-notifier-1.4/protocol/meson.build000066400000000000000000000023701351432077500201360ustar00rootroot00000000000000wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') wayland_scanner = find_program('wayland-scanner') # should check wayland_scanner's version, but it is hard to get if wayland_client.version().version_compare('>=1.14.91') code_type = 'private-code' else code_type = 'code' endif wayland_scanner_code = generator( wayland_scanner, output: '@BASENAME@-protocol.c', arguments: [code_type, '@INPUT@', '@OUTPUT@'], ) wayland_scanner_client = generator( wayland_scanner, output: '@BASENAME@-client-protocol.h', arguments: ['client-header', '@INPUT@', '@OUTPUT@'], ) client_protocols = [ [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], ['wlr-layer-shell-unstable-v1.xml'], ] client_protos_src = [] client_protos_headers = [] foreach p : client_protocols xml = join_paths(p) client_protos_src += wayland_scanner_code.process(xml) client_protos_headers += wayland_scanner_client.process(xml) endforeach lib_client_protos = static_library( 'client_protos', client_protos_src + client_protos_headers, dependencies: [wayland_client] ) # for the include directory client_protos = declare_dependency( link_with: lib_client_protos, sources: client_protos_headers, ) mako-notifier-1.4/protocol/wlr-layer-shell-unstable-v1.xml000066400000000000000000000320701351432077500237000ustar00rootroot00000000000000 Copyright © 2017 Drew DeVault Permission to use, copy, modify, distribute, and sell this software and its documentation for any purpose is hereby granted without fee, provided that the above copyright notice appear in all copies and that both that copyright notice and this permission notice appear in supporting documentation, and that the name of the copyright holders not be used in advertising or publicity pertaining to distribution of the software without specific, written prior permission. The copyright holders make no representations about the suitability of this software for any purpose. It is provided "as is" without express or implied warranty. THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and rendered with a defined z-depth respective to each other. They may also be anchored to the edges and corners of a screen and specify input handling semantics. This interface should be suitable for the implementation of many desktop shell components, and a broad number of other applications that interact with the desktop. Create a layer surface for an existing surface. This assigns the role of layer_surface, or raises a protocol error if another role is already assigned. Creating a layer surface from a wl_surface which has a buffer attached or committed is a client error, and any attempts by a client to attach or manipulate a buffer prior to the first layer_surface.configure call must also be treated as errors. You may pass NULL for output to allow the compositor to decide which output to use. Generally this will be the one that the user most recently interacted with. Clients can specify a namespace that defines the purpose of the layer surface. These values indicate which layers a surface can be rendered in. They are ordered by z depth, bottom-most first. Traditional shell surfaces will typically be rendered between the bottom and top layers. Fullscreen shell surfaces are typically rendered at the top layer. Multiple surfaces can share a single layer, and ordering within a single layer is undefined. An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like environment. Layer surface state (size, anchor, exclusive zone, margin, interactivity) is double-buffered, and will be applied at the time wl_surface.commit of the corresponding wl_surface is called. Sets the size of the surface in surface-local coordinates. The compositor will display the surface centered with respect to its anchors. If you pass 0 for either value, the compositor will assign it and inform you of the assignment in the configure event. You must set your anchor to opposite edges in the dimensions you omit; not doing so is a protocol error. Both values are 0 by default. Size is double-buffered, see wl_surface.commit. Requests that the compositor anchor the surface to the specified edges and corners. If two orthoginal edges are specified (e.g. 'top' and 'left'), then the anchor point will be the intersection of the edges (e.g. the top left corner of the output); otherwise the anchor point will be centered on that edge, or in the center if none is specified. Anchor is double-buffered, see wl_surface.commit. Requests that the compositor avoids occluding an area of the surface with other surfaces. The compositor's use of this information is implementation-dependent - do not assume that this region will not actually be occluded. A positive value is only meaningful if the surface is anchored to an edge, rather than a corner. The zone is the number of surface-local coordinates from the edge that are considered exclusive. Surfaces that do not wish to have an exclusive zone may instead specify how they should interact with surfaces that do. If set to zero, the surface indicates that it would like to be moved to avoid occluding surfaces with a positive excluzive zone. If set to -1, the surface indicates that it would not like to be moved to accomodate for other surfaces, and the compositor should extend it all the way to the edges it is anchored to. For example, a panel might set its exclusive zone to 10, so that maximized shell surfaces are not shown on top of it. A notification might set its exclusive zone to 0, so that it is moved to avoid occluding the panel, but shell surfaces are shown underneath it. A wallpaper or lock screen might set their exclusive zone to -1, so that they stretch below or over the panel. The default value is 0. Exclusive zone is double-buffered, see wl_surface.commit. Requests that the surface be placed some distance away from the anchor point on the output, in surface-local coordinates. Setting this value for edges you are not anchored to has no effect. The exclusive zone includes the margin. Margin is double-buffered, see wl_surface.commit. Set to 1 to request that the seat send keyboard events to this layer surface. For layers below the shell surface layer, the seat will use normal focus semantics. For layers above the shell surface layers, the seat will always give exclusive keyboard focus to the top-most layer which has keyboard interactivity set to true. Layer surfaces receive pointer, touch, and tablet events normally. If you do not want to receive them, set the input region on your surface to an empty region. Events is double-buffered, see wl_surface.commit. This assigns an xdg_popup's parent to this layer_surface. This popup should have been created via xdg_surface::get_popup with the parent set to NULL, and this request must be invoked before committing the popup's initial state. See the documentation of xdg_popup for more details about what an xdg_popup is and how it is used. When a configure event is received, if a client commits the surface in response to the configure event, then the client must make an ack_configure request sometime before the commit request, passing along the serial of the configure event. If the client receives multiple configure events before it can respond to one, it only has to ack the last configure event. A client is not required to commit immediately after sending an ack_configure request - it may even ack_configure several times before its next surface commit. A client may send multiple ack_configure requests before committing, but only the last request sent before a commit indicates which configure event the client really is responding to. This request destroys the layer surface. The configure event asks the client to resize its surface. Clients should arrange their surface for the new states, and then send an ack_configure request with the serial sent in this configure event at some point before committing the new surface. The client is free to dismiss all but the last configure event it received. The width and height arguments specify the size of the window in surface-local coordinates. The size is a hint, in the sense that the client is free to ignore it if it doesn't resize, pick a smaller size (to satisfy aspect ratio or resize in steps of NxM pixels). If the client picks a smaller size and is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the surface will be centered on this axis. If the width or height arguments are zero, it means the client should decide its own window dimension. The closed event is sent by the compositor when the surface will no longer be shown. The output may have been destroyed or the user may have asked for it to be removed. Further changes to the surface will be ignored. The client should destroy the resource after receiving this event, and create a new surface if they so choose. mako-notifier-1.4/render.c000066400000000000000000000242131351432077500155560ustar00rootroot00000000000000#define _XOPEN_SOURCE 700 #include #include #include #include #include #include "config.h" #include "criteria.h" #include "mako.h" #include "notification.h" #include "render.h" #include "wayland.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "icon.h" // HiDPI conventions: local variables are in surface-local coordinates, unless // they have a "buffer_" prefix, in which case they are in buffer-local // coordinates. static void set_source_u32(cairo_t *cairo, uint32_t color) { cairo_set_source_rgba(cairo, (color >> (3*8) & 0xFF) / 255.0, (color >> (2*8) & 0xFF) / 255.0, (color >> (1*8) & 0xFF) / 255.0, (color >> (0*8) & 0xFF) / 255.0); } static void set_layout_size(PangoLayout *layout, int width, int height, int scale) { pango_layout_set_width(layout, width * scale * PANGO_SCALE); pango_layout_set_height(layout, height * scale * PANGO_SCALE); } static void move_to(cairo_t *cairo, double x, double y, int scale) { cairo_move_to(cairo, x * scale, y * scale); } static void set_rounded_rectangle(cairo_t *cairo, double x, double y, double width, double height, int scale, int radius) { if (width == 0 || height == 0) { return; } x *= scale; y *= scale; width *= scale; height *= scale; double degrees = M_PI / 180.0; if (width < radius * 2) { width = radius * 2; } if (height < radius * 2) { height = radius * 2; } cairo_new_sub_path(cairo); cairo_arc(cairo, x + width - radius, y + radius, radius, -90 * degrees, 0 * degrees); cairo_arc(cairo, x + width - radius, y + height - radius, radius, 0 * degrees, 90 * degrees); cairo_arc(cairo, x + radius, y + height - radius, radius, 90 * degrees, 180 * degrees); cairo_arc(cairo, x + radius, y + radius, radius, 180 * degrees, 270 * degrees); cairo_close_path(cairo); } static cairo_subpixel_order_t get_cairo_subpixel_order( enum wl_output_subpixel subpixel) { switch (subpixel) { case WL_OUTPUT_SUBPIXEL_UNKNOWN: case WL_OUTPUT_SUBPIXEL_NONE: return CAIRO_SUBPIXEL_ORDER_DEFAULT; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: return CAIRO_SUBPIXEL_ORDER_RGB; case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: return CAIRO_SUBPIXEL_ORDER_BGR; case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: return CAIRO_SUBPIXEL_ORDER_VRGB; case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: return CAIRO_SUBPIXEL_ORDER_VBGR; } assert(0); } static void set_font_options(cairo_t *cairo, struct mako_state *state) { if (state->surface_output == NULL) { return; } cairo_font_options_t *fo = cairo_font_options_create(); cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_SUBPIXEL); cairo_font_options_set_subpixel_order(fo, get_cairo_subpixel_order(state->surface_output->subpixel)); cairo_set_font_options(cairo, fo); cairo_font_options_destroy(fo); } static int render_notification(cairo_t *cairo, struct mako_state *state, struct mako_style *style, const char *text, struct mako_icon *icon, int offset_y, int scale, struct mako_hotspot *hotspot, int progress) { int border_size = 2 * style->border_size; int padding_height = style->padding.top + style->padding.bottom; int padding_width = style->padding.left + style->padding.right; int radius = style->border_radius; // If the compositor has forced us to shrink down, do so. int notif_width = (style->width <= state->width) ? style->width : state->width; int offset_x; if (state->config.anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT) { offset_x = state->width - notif_width - style->margin.right; } else { offset_x = style->margin.left; } double text_x = style->padding.left; if (icon != NULL) { text_x = icon->width + 2*style->padding.left; } set_font_options(cairo, state); PangoLayout *layout = pango_cairo_create_layout(cairo); set_layout_size(layout, notif_width - border_size - padding_width - text_x, style->height - border_size - padding_height, scale); pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); PangoFontDescription *desc = pango_font_description_from_string(style->font); pango_layout_set_font_description(layout, desc); pango_font_description_free(desc); PangoAttrList *attrs = NULL; GError *error = NULL; char *buf = NULL; if (pango_parse_markup(text, -1, 0, &attrs, &buf, NULL, &error)) { pango_layout_set_text(layout, buf, -1); free(buf); } else { fprintf(stderr, "cannot parse pango markup: %s\n", error->message); g_error_free(error); // fallback to plain text pango_layout_set_text(layout, text, -1); } if (attrs == NULL) { attrs = pango_attr_list_new(); } pango_attr_list_insert(attrs, pango_attr_scale_new(scale)); pango_layout_set_attributes(layout, attrs); pango_attr_list_unref(attrs); int buffer_text_height = 0; // If there's no text to be rendered, the notification can shrink down // smaller than the line height. if (pango_layout_get_character_count(layout) > 0) { pango_layout_get_pixel_size(layout, NULL, &buffer_text_height); } int text_height = buffer_text_height / scale; int notif_height = text_height + border_size + padding_height; if (icon != NULL && icon->height > text_height) { notif_height = icon->height + border_size + padding_height; } // Render border set_source_u32(cairo, style->colors.border); set_rounded_rectangle(cairo, offset_x + style->border_size / 2.0, offset_y + style->border_size / 2.0, notif_width - style->border_size, notif_height - style->border_size, scale, radius); cairo_save(cairo); cairo_set_line_width(cairo, style->border_size * scale); cairo_stroke_preserve(cairo); cairo_restore(cairo); int notif_background_width = notif_width - border_size; int progress_width = notif_background_width * progress / 100; if (progress_width < 0) { progress_width = 0; } else if (progress_width > notif_background_width) { progress_width = notif_background_width; } // Render background set_source_u32(cairo, style->colors.background); cairo_fill(cairo); // Render progress cairo_save(cairo); cairo_set_operator(cairo, style->colors.progress.operator); set_source_u32(cairo, style->colors.progress.value); set_rounded_rectangle(cairo, offset_x + style->border_size, offset_y + style->border_size, progress_width, notif_height - border_size, scale, radius); cairo_fill(cairo); cairo_restore(cairo); if (icon != NULL) { // Render icon double xpos = offset_x + style->border_size + (text_x - icon->width) / 2; double ypos = offset_y + style->border_size + (notif_height - icon->height - border_size) / 2; draw_icon(cairo, icon, xpos, ypos, scale); } // Render text set_source_u32(cairo, style->colors.text); move_to(cairo, offset_x + style->border_size + text_x, offset_y + style->border_size + (double)(notif_height - border_size - text_height) / 2, scale); pango_cairo_update_layout(cairo, layout); pango_cairo_show_layout(cairo, layout); // Update hotspot with calculated location if (hotspot != NULL) { hotspot->x = offset_x; hotspot->y = offset_y; hotspot->width = notif_width; hotspot->height = notif_height; } g_object_unref(layout); return notif_height; } int render(struct mako_state *state, struct pool_buffer *buffer, int scale) { struct mako_config *config = &state->config; cairo_t *cairo = buffer->cairo; if (wl_list_empty(&state->notifications)) { return 0; } // Clear cairo_save(cairo); cairo_set_source_rgba(cairo, 0, 0, 0, 0); cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); cairo_paint(cairo); cairo_restore(cairo); size_t i = 0; size_t visible_count = 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; ++i; // We count how many we've seen even if we're not rendering them. if (style->invisible) { continue; } size_t text_len = format_text(style->format, NULL, format_notif_text, notif); char *text = malloc(text_len + 1); if (text == NULL) { fprintf(stderr, "Unable to allocate memory to render notification\n"); break; } format_text(style->format, text, format_notif_text, notif); if (style->margin.top > pending_bottom_margin) { total_height += style->margin.top; } else { total_height += pending_bottom_margin; } struct mako_icon *icon = (style->icons) ? notif->icon : NULL; int notif_height = render_notification( cairo, state, style, text, icon, total_height, scale, ¬if->hotspot, notif->progress); free(text); total_height += notif_height; pending_bottom_margin = style->margin.bottom; if (notif->group_index < 1) { // If the notification is ungrouped, or is the first in a group, it // counts against max_visible. Even if other notifications in the // group are rendered based on criteria, a group is considered a // single entity for this purpose. ++visible_count; } if (config->max_visible >= 0 && visible_count >= (size_t)config->max_visible) { break; } } size_t count = wl_list_length(&state->notifications); if (count > i) { // 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; } struct mako_hidden_format_data data = { .hidden = count - i, .count = count, }; size_t text_ln = format_text(style.format, NULL, format_hidden_text, &data); char *text = malloc(text_ln + 1); if (text == NULL) { fprintf(stderr, "allocation failed"); return 0; } format_text(style.format, text, format_hidden_text, &data); int hidden_height = render_notification( cairo, state, &style, text, NULL, total_height, scale, NULL, 0); free(text); finish_style(&style); total_height += hidden_height; pending_bottom_margin = style.margin.bottom; } return total_height + pending_bottom_margin; } mako-notifier-1.4/string-util.c000066400000000000000000000006311351432077500165560ustar00rootroot00000000000000#include #include #include char *mako_asprintf(const char *fmt, ...) { char *text; va_list args; va_start(args, fmt); int size = vsnprintf(NULL, 0, fmt, args); va_end(args); if (size < 0) { return NULL; } text = malloc(size + 1); if (text == NULL) { return NULL; } va_start(args, fmt); vsnprintf(text, size + 1, fmt, args); va_end(args); return text; } mako-notifier-1.4/types.c000066400000000000000000000142001351432077500154360ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include "enum.h" #include "types.h" const char VALID_FORMAT_SPECIFIERS[] = "%asbhtg"; 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_int(const char *string, int *out) { errno = 0; char *end; *out = (int)strtol(string, &end, 10); return errno == 0 && end[0] == '\0'; } bool parse_color(const char *string, uint32_t *out) { if (string[0] != '#') { return false; } string++; size_t len = strlen(string); if (len != 6 && len != 8) { return false; } errno = 0; char *end; *out = (uint32_t)strtoul(string, &end, 16); if (errno != 0 || end[0] != '\0') { return false; } if (len == 6) { *out = (*out << 8) | 0xFF; } return true; } bool parse_mako_color(const char *string, struct mako_color *out) { char *components = strdup(string); char *saveptr = NULL; char *token = strtok_r(components, " \t", &saveptr); if (token[0] == '#') { out->operator = CAIRO_OPERATOR_OVER; } else { if (strcasecmp(token, "over") == 0) { out->operator = CAIRO_OPERATOR_OVER; } else if (strcasecmp(token, "source") == 0) { out->operator = CAIRO_OPERATOR_SOURCE; } else { free(components); return false; } token = strtok_r(NULL, " \t", &saveptr); if (token == NULL) { return false; } } bool ok = parse_color(token, &out->value); free(components); return ok; } 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; } /* Parse between 1 and 4 integers, comma separated, from the provided string. * Depending on the number of integers provided, the four fields of the `out` * struct will be initialized following the same rules as the CSS "margin" * property. */ bool parse_directional(const char *string, struct mako_directional *out) { char *components = strdup(string); int32_t values[] = {0, 0, 0, 0}; char *saveptr = NULL; char *token = strtok_r(components, ",", &saveptr); size_t count; for (count = 0; count < 4; count++) { if (token == NULL) { break; } int32_t number; if (!parse_int(token, &number)) { // There were no digits, or something else went horribly wrong free(components); return false; } values[count] = number; token = strtok_r(NULL, ",", &saveptr); } switch (count) { case 1: // All values are the same out->top = out->right = out->bottom = out->left = values[0]; break; case 2: // Vertical, horizontal out->top = out->bottom = values[0]; out->right = out->left = values[1]; break; case 3: // Top, horizontal, bottom out->top = values[0]; out->right = out->left = values[1]; out->bottom = values[2]; break; case 4: // Top, right, bottom, left out->top = values[0]; out->right = values[1]; out->bottom = values[2]; out->left = values[3]; break; } free(components); return true; } bool parse_criteria_spec(const char *string, struct mako_criteria_spec *out) { // Clear any existing specified fields in the output spec. memset(out, 0, sizeof(struct mako_criteria_spec)); char *components = strdup(string); char *saveptr = NULL; char *token = strtok_r(components, ",", &saveptr); while (token) { // Can't just use &= because then we nave no way to report invalid // values. :( if (strcmp(token, "app-name") == 0) { out->app_name = true; } else if (strcmp(token, "app-icon") == 0) { out->app_icon = true; } else if (strcmp(token, "actionable") == 0) { out->actionable = true; } else if (strcmp(token, "expiring") == 0) { out->expiring = true; } else if (strcmp(token, "urgency") == 0) { out->urgency = true; } else if (strcmp(token, "category") == 0) { out->category = true; } else if (strcmp(token, "desktop-entry") == 0) { out->desktop_entry = true; } else if (strcmp(token, "summary") == 0) { out->summary = true; } else if (strcmp(token, "body") == 0) { out->body = true; } else if (strcmp(token, "none") == 0) { out->none = true; } else { fprintf(stderr, "Unknown criteria field '%s'\n", token); return false; } token = strtok_r(NULL, ",", &saveptr); } return true; } bool parse_format(const char *string, char **out) { size_t 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; for (size_t i = 0; i < token_max_length; ++i) { char ch = string[i]; switch (state) { case MAKO_PARSE_STATE_FORMAT: if (!strchr(VALID_FORMAT_SPECIFIERS, ch)) { // There's an invalid format specifier, bail. *out = NULL; return false; } token[token_location] = ch; ++token_location; state = MAKO_PARSE_STATE_NORMAL; break; case MAKO_PARSE_STATE_ESCAPE: switch (ch) { case 'n': token[token_location] = '\n'; ++token_location; break; case '\\': token[token_location] = '\\'; ++token_location; break; default: ++token_location; token[token_location] = ch; ++token_location; break; } state = MAKO_PARSE_STATE_NORMAL; break; case MAKO_PARSE_STATE_NORMAL: switch (ch) { case '\\': token[token_location] = ch; state = MAKO_PARSE_STATE_ESCAPE; break; case '%': token[token_location] = ch; ++token_location; // Leave the % intact. state = MAKO_PARSE_STATE_FORMAT; break; default: token[token_location] = ch; ++token_location; break; } break; default: *out = NULL; return false; } } *out = strdup(token); return true; } mako-notifier-1.4/wayland.c000066400000000000000000000426501351432077500157430ustar00rootroot00000000000000#define _POSIX_C_SOURCE 200809L #include #include #include #include #include "criteria.h" #include "mako.h" #include "notification.h" #include "render.h" #include "wayland.h" static void noop() { // This space intentionally left blank } static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) { struct mako_output *output = data; output->name = strdup(name); } static const struct zxdg_output_v1_listener xdg_output_listener = { .logical_position = noop, .logical_size = noop, .done = noop, .name = xdg_output_handle_name, .description = noop, }; static void get_xdg_output(struct mako_output *output) { if (output->state->xdg_output_manager == NULL || output->xdg_output != NULL) { return; } output->xdg_output = zxdg_output_manager_v1_get_xdg_output( output->state->xdg_output_manager, output->wl_output); zxdg_output_v1_add_listener(output->xdg_output, &xdg_output_listener, output); } static void output_handle_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, int32_t phy_width, int32_t phy_height, int32_t subpixel, const char *make, const char *model, int32_t transform) { struct mako_output *output = data; output->subpixel = subpixel; } static void output_handle_scale(void *data, struct wl_output *wl_output, int32_t factor) { struct mako_output *output = data; output->scale = factor; } static const struct wl_output_listener output_listener = { .geometry = output_handle_geometry, .mode = noop, .done = noop, .scale = output_handle_scale, }; static void create_output(struct mako_state *state, struct wl_output *wl_output, uint32_t global_name) { struct mako_output *output = calloc(1, sizeof(struct mako_output)); if (output == NULL) { fprintf(stderr, "allocation failed\n"); return; } output->state = state; output->global_name = global_name; output->wl_output = wl_output; output->scale = 1; wl_list_insert(&state->outputs, &output->link); wl_output_set_user_data(wl_output, output); wl_output_add_listener(wl_output, &output_listener, output); get_xdg_output(output); } static void destroy_output(struct mako_output *output) { if (output->state->surface_output == output) { output->state->surface_output = NULL; } if (output->state->layer_surface_output == output) { output->state->layer_surface_output = NULL; } wl_list_remove(&output->link); if (output->xdg_output != NULL) { zxdg_output_v1_destroy(output->xdg_output); } wl_output_destroy(output->wl_output); free(output->name); free(output); } static void touch_handle_motion(void *data, struct wl_touch *wl_touch, uint32_t time, int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct mako_seat *seat = data; seat->touch.x = wl_fixed_to_int(surface_x); seat->touch.y = wl_fixed_to_int(surface_y); } static void touch_handle_down(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, struct wl_surface *sfc, int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct mako_seat *seat = data; seat->touch.x = wl_fixed_to_int(surface_x); seat->touch.y = wl_fixed_to_int(surface_y); } static void touch_handle_up(void *data, struct wl_touch *wl_touch, uint32_t serial, uint32_t time, int32_t id) { struct mako_seat *seat = data; struct mako_state *state = seat->state; struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (hotspot_at(¬if->hotspot, seat->touch.x, seat->touch.y)) { notification_handle_touch(notif); break; } } set_dirty(state); } static void pointer_handle_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct mako_seat *seat = data; seat->pointer.x = wl_fixed_to_int(surface_x); seat->pointer.y = wl_fixed_to_int(surface_y); } static void pointer_handle_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t button_state) { struct mako_seat *seat = data; struct mako_state *state = seat->state; struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (hotspot_at(¬if->hotspot, seat->pointer.x, seat->pointer.y)) { notification_handle_button(notif, button, button_state); break; } } set_dirty(state); } static const struct wl_pointer_listener pointer_listener = { .enter = noop, .leave = noop, .motion = pointer_handle_motion, .button = pointer_handle_button, .axis = noop, }; static const struct wl_touch_listener touch_listener = { .down = touch_handle_down, .up = touch_handle_up, .motion = touch_handle_motion, .frame = noop, .cancel = noop, .shape = noop, .orientation = noop, }; static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, uint32_t capabilities) { struct mako_seat *seat = data; if (seat->pointer.wl_pointer != NULL) { wl_pointer_release(seat->pointer.wl_pointer); seat->pointer.wl_pointer = NULL; } if (capabilities & WL_SEAT_CAPABILITY_POINTER) { seat->pointer.wl_pointer = wl_seat_get_pointer(wl_seat); wl_pointer_add_listener(seat->pointer.wl_pointer, &pointer_listener, seat); } if (seat->touch.wl_touch != NULL) { wl_touch_release(seat->touch.wl_touch); seat->touch.wl_touch = NULL; } if (capabilities & WL_SEAT_CAPABILITY_TOUCH) { seat->touch.wl_touch = wl_seat_get_touch(wl_seat); wl_touch_add_listener(seat->touch.wl_touch, &touch_listener, seat); } } static const struct wl_seat_listener seat_listener = { .capabilities = seat_handle_capabilities, .name = noop, }; static void create_seat(struct mako_state *state, struct wl_seat *wl_seat) { struct mako_seat *seat = calloc(1, sizeof(struct mako_seat)); if (seat == NULL) { fprintf(stderr, "allocation failed\n"); return; } seat->state = state; seat->wl_seat = wl_seat; wl_list_insert(&state->seats, &seat->link); wl_seat_add_listener(wl_seat, &seat_listener, seat); } static void destroy_seat(struct mako_seat *seat) { wl_list_remove(&seat->link); wl_seat_release(seat->wl_seat); if (seat->pointer.wl_pointer) { wl_pointer_release(seat->pointer.wl_pointer); } free(seat); } static void surface_handle_enter(void *data, struct wl_surface *surface, struct wl_output *wl_output) { struct mako_state *state = data; // Don't bother keeping a list of outputs, a layer surface can only be on // one output a a time state->surface_output = wl_output_get_user_data(wl_output); set_dirty(state); } static void surface_handle_leave(void *data, struct wl_surface *surface, struct wl_output *wl_output) { struct mako_state *state = data; state->surface_output = NULL; } static const struct wl_surface_listener surface_listener = { .enter = surface_handle_enter, .leave = surface_handle_leave, }; static void send_frame(struct mako_state *state); static void layer_surface_handle_configure(void *data, struct zwlr_layer_surface_v1 *surface, uint32_t serial, uint32_t width, uint32_t height) { struct mako_state *state = data; state->configured = true; state->width = width; state->height = height; zwlr_layer_surface_v1_ack_configure(surface, serial); send_frame(state); } static void layer_surface_handle_closed(void *data, struct zwlr_layer_surface_v1 *surface) { struct mako_state *state = data; zwlr_layer_surface_v1_destroy(state->layer_surface); state->layer_surface = NULL; wl_surface_destroy(state->surface); state->surface = NULL; if (state->configured) { state->configured = false; state->width = state->height = 0; set_dirty(state); } } static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { .configure = layer_surface_handle_configure, .closed = layer_surface_handle_closed, }; static void handle_global(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { struct mako_state *state = data; if (strcmp(interface, wl_compositor_interface.name) == 0) { state->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4); } else if (strcmp(interface, wl_shm_interface.name) == 0) { state->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { state->layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1); } else if (strcmp(interface, wl_seat_interface.name) == 0) { struct wl_seat *seat = wl_registry_bind(registry, name, &wl_seat_interface, 3); create_seat(state, seat); } else if (strcmp(interface, wl_output_interface.name) == 0) { struct wl_output *output = wl_registry_bind(registry, name, &wl_output_interface, 3); create_output(state, output, name); } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0 && version >= ZXDG_OUTPUT_V1_NAME_SINCE_VERSION) { state->xdg_output_manager = wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, ZXDG_OUTPUT_V1_NAME_SINCE_VERSION); } } static void handle_global_remove(void *data, struct wl_registry *registry, uint32_t name) { struct mako_state *state = data; struct mako_output *output, *tmp; wl_list_for_each_safe(output, tmp, &state->outputs, link) { if (output->global_name == name) { destroy_output(output); break; } } } static const struct wl_registry_listener registry_listener = { .global = handle_global, .global_remove = handle_global_remove, }; bool init_wayland(struct mako_state *state) { wl_list_init(&state->outputs); wl_list_init(&state->seats); state->display = wl_display_connect(NULL); if (state->display == NULL) { fprintf(stderr, "failed to create display\n"); return false; } state->registry = wl_display_get_registry(state->display); wl_registry_add_listener(state->registry, ®istry_listener, state); wl_display_roundtrip(state->display); if (state->compositor == NULL) { fprintf(stderr, "compositor doesn't support wl_compositor\n"); return false; } if (state->shm == NULL) { fprintf(stderr, "compositor doesn't support wl_shm\n"); return false; } if (state->layer_shell == NULL) { fprintf(stderr, "compositor doesn't support zwlr_layer_shell_v1\n"); return false; } if (state->xdg_output_manager != NULL) { struct mako_output *output; wl_list_for_each(output, &state->outputs, link) { get_xdg_output(output); } wl_display_roundtrip(state->display); } if (state->xdg_output_manager == NULL && strcmp(state->config.output, "") != 0) { fprintf(stderr, "warning: configured an output but compositor doesn't " "support xdg-output-unstable-v1 version 2\n"); } return true; } void finish_wayland(struct mako_state *state) { if (state->layer_surface != NULL) { zwlr_layer_surface_v1_destroy(state->layer_surface); } if (state->surface != NULL) { wl_surface_destroy(state->surface); } finish_buffer(&state->buffers[0]); finish_buffer(&state->buffers[1]); struct mako_output *output, *output_tmp; wl_list_for_each_safe(output, output_tmp, &state->outputs, link) { destroy_output(output); } struct mako_seat *seat, *seat_tmp; wl_list_for_each_safe(seat, seat_tmp, &state->seats, link) { destroy_seat(seat); } if (state->xdg_output_manager != NULL) { zxdg_output_manager_v1_destroy(state->xdg_output_manager); } zwlr_layer_shell_v1_destroy(state->layer_shell); wl_compositor_destroy(state->compositor); wl_shm_destroy(state->shm); wl_registry_destroy(state->registry); wl_display_disconnect(state->display); } static struct wl_region *get_input_region(struct mako_state *state) { struct wl_region *region = wl_compositor_create_region(state->compositor); struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { struct mako_hotspot *hotspot = ¬if->hotspot; wl_region_add(region, hotspot->x, hotspot->y, hotspot->width, hotspot->height); } return region; } static struct mako_output *get_configured_output(struct mako_state *state) { const char *output_name = state->config.output; if (strcmp(output_name, "") == 0) { return NULL; } struct mako_output *output; wl_list_for_each(output, &state->outputs, link) { if (output->name != NULL && strcmp(output->name, output_name) == 0) { return output; } } return NULL; } static void schedule_frame_and_commit(struct mako_state *state); // Draw and commit a new frame. static void send_frame(struct mako_state *state) { int scale = 1; if (state->surface_output != NULL) { scale = state->surface_output->scale; } state->current_buffer = get_next_buffer(state->shm, state->buffers, state->width * scale, state->height * scale); if (state->current_buffer == NULL) { fprintf(stderr, "no buffer available\n"); return; } struct mako_output *output = get_configured_output(state); int height = render(state, state->current_buffer, scale); // There are two cases where we want to tear down the surface: zero // notifications (height = 0) or moving between outputs. if (height == 0 || state->layer_surface_output != output) { if (state->layer_surface != NULL) { zwlr_layer_surface_v1_destroy(state->layer_surface); state->layer_surface = NULL; } if (state->surface != NULL) { wl_surface_destroy(state->surface); state->surface = NULL; } state->width = state->height = 0; state->surface_output = NULL; state->configured = false; } // If there are no notifications, there's no point in recreating the // surface right now. if (height == 0) { state->dirty = false; return; } // If we've made it here, there is something to draw. If the surface // doesn't exist (this is the first notification, or we moved to a // different output), we need to create it. if (state->layer_surface == NULL) { struct wl_output *wl_output = NULL; if (output != NULL) { wl_output = output->wl_output; } state->layer_surface_output = output; state->surface = wl_compositor_create_surface(state->compositor); wl_surface_add_listener(state->surface, &surface_listener, state); state->layer_surface = zwlr_layer_shell_v1_get_layer_surface( state->layer_shell, state->surface, wl_output, state->config.layer, "notifications"); zwlr_layer_surface_v1_add_listener(state->layer_surface, &layer_surface_listener, state); // Because we're creating a new surface, we aren't going to draw // anything into it during this call. We don't know what size the // surface will be until we've asked the compositor for what we want // and it has responded with what it actually gave us. We also know // that the height we would _like_ to draw (greater than zero, or we // would have bailed already) is different from our state->height // (which has to be zero here), so we can fall through to the next // block to let it set the size for us. } assert(state->layer_surface); // We now want to resize the surface if it isn't the right size. If the // surface is brand new, it doesn't even have a size yet. If it already // exists, we might need to resize if the list of notifications has changed // since the last time we drew. if (state->height != height) { struct mako_style *style = &state->config.superstyle; zwlr_layer_surface_v1_set_size(state->layer_surface, style->width + style->margin.left + style->margin.right, height); zwlr_layer_surface_v1_set_anchor(state->layer_surface, state->config.anchor); wl_surface_commit(state->surface); // Now we're going to bail without drawing anything. This gives the // compositor a chance to create the surface and tell us what size we // were actually granted, which may be smaller than what we asked for // depending on the screen size and layout of other layer surfaces. // This information is provided in layer_surface_handle_configure, // which will then call send_frame again. When that call happens, the // layer surface will exist and the height will hopefully match what // we asked for. That means we won't return here, and will actually // draw into the surface down below. // TODO: If the compositor doesn't send a configure with the size we // requested, we'll enter an infinite loop. We need to keep track of // the fact that a request was sent separately from what height we are. return; } assert(state->configured); // Yay we can finally draw something! struct wl_region *input_region = get_input_region(state); wl_surface_set_input_region(state->surface, input_region); wl_region_destroy(input_region); wl_surface_set_buffer_scale(state->surface, scale); wl_surface_damage_buffer(state->surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_attach(state->surface, state->current_buffer->buffer, 0, 0); state->current_buffer->busy = true; // Schedule a frame in case the state becomes dirty again schedule_frame_and_commit(state); state->dirty = false; } static void frame_handle_done(void *data, struct wl_callback *callback, uint32_t time) { struct mako_state *state = data; wl_callback_destroy(callback); state->frame_pending = false; // Only draw again if we need to if (state->dirty) { send_frame(state); } } static const struct wl_callback_listener frame_listener = { .done = frame_handle_done, }; static void schedule_frame_and_commit(struct mako_state *state) { if (state->frame_pending) { return; } if (state->surface == NULL) { // We don't yet have a surface, create it immediately send_frame(state); return; } struct wl_callback *callback = wl_surface_frame(state->surface); wl_callback_add_listener(callback, &frame_listener, state); wl_surface_commit(state->surface); state->frame_pending = true; } void set_dirty(struct mako_state *state) { if (state->dirty) { return; } state->dirty = true; schedule_frame_and_commit(state); }