pax_global_header00006660000000000000000000000064147655571250014533gustar00rootroot0000000000000052 comment=b131bc143f6b0f24d650f16bb88a11c7cb011c20 mako-notifier-1.10.0/000077500000000000000000000000001476555712500143565ustar00rootroot00000000000000mako-notifier-1.10.0/.builds/000077500000000000000000000000001476555712500157165ustar00rootroot00000000000000mako-notifier-1.10.0/.builds/alpine.yml000066400000000000000000000005371476555712500177160ustar00rootroot00000000000000image: alpine/edge packages: - elogind-dev - meson - wayland-dev - wayland-protocols - cairo-dev - pango-dev - gdk-pixbuf-dev - scdoc sources: - https://github.com/emersion/mako tasks: - setup: | cd mako meson build/ -Dauto_features=enabled -Dsd-bus-provider=libelogind - build: | cd mako ninja -C build/ mako-notifier-1.10.0/.builds/archlinux.yml000066400000000000000000000006561476555712500204450ustar00rootroot00000000000000image: 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 -Dsd-bus-provider=libsystemd - build: | cd mako ninja -C build - build-no-icons: | cd mako meson configure build -Dicons=disabled ninja -C build mako-notifier-1.10.0/.builds/freebsd.yml000066400000000000000000000005521476555712500200550ustar00rootroot00000000000000image: freebsd/latest packages: - basu - evdev-proto - gdk-pixbuf2 - libepoll-shim - meson - pango - pkgconf - scdoc - wayland - wayland-protocols sources: - https://github.com/emersion/mako tasks: - setup: | cd mako meson build/ -Dauto_features=enabled -Dsd-bus-provider=basu - build: | cd mako ninja -C build/ mako-notifier-1.10.0/.editorconfig000066400000000000000000000002011476555712500170240ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf indent_style = tab insert_final_newline = true trim_trailing_whitespace = true mako-notifier-1.10.0/.gitignore000066400000000000000000000000161476555712500163430ustar00rootroot00000000000000/subprojects/ mako-notifier-1.10.0/LICENSE000066400000000000000000000020511476555712500153610ustar00rootroot00000000000000MIT 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.10.0/README.md000066400000000000000000000036171476555712500156440ustar00rootroot00000000000000# mako A lightweight notification daemon for Wayland. Works on Sway.

mako screenshot

mako implements the [FreeDesktop Notifications Specification][spec]. Feel free to join the IRC channel: #emersion on irc.libera.chat. ## Running `mako` will run automatically when a notification is emitted. This happens via D-Bus activation, so you don't really need to explicitly start it up (this also allows delaying its startup time and speed up system startup). If you have several notification daemons installed though, you might want to explicitly start this one. Some ways of achieving this is: - If you're using Sway you can start mako on launch by putting `exec mako` in your configuration file. - If you are not using systemd, you might need to manually start a dbus user session: `dbus-daemon --session --address=unix:path=$XDG_RUNTIME_DIR/bus` ## Configuration `mako` can be extensively configured and customized - feel free to read more using the command `man 5 mako` For control of mako during runtime, `makoctl` can be used; see `man makoctl` ## Building Install dependencies: * meson (build-time dependency) * wayland * pango * cairo * systemd, elogind or [basu] (for the sd-bus library) * gdk-pixbuf (optional, for icons support) * dbus (runtime dependency, user-session support is required) * scdoc (optional, for man pages) Then run: ```shell meson build ninja -C build build/mako ```

mako

## I have a question! See the [faq section in the wiki](https://github.com/emersion/mako/wiki/Frequently-asked-questions). ## License MIT [spec]: https://specifications.freedesktop.org/notification-spec/latest/ [basu]: https://github.com/emersion/basu mako-notifier-1.10.0/cairo-pixbuf.c000066400000000000000000000047261476555712500171230ustar00rootroot00000000000000#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.10.0/config.c000066400000000000000000000750371476555712500160030ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "config.h" #include "criteria.h" #include "string-util.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)"); // Define the default format for the hidden placeholder notification. new_criteria = create_criteria(config); init_empty_style(&new_criteria->style); new_criteria->hidden = true; new_criteria->spec.hidden = true; new_criteria->style.format = strdup("(%h more)"); new_criteria->style.spec.format = true; new_criteria->raw_string = strdup("(default hidden)"); init_empty_style(&config->superstyle); config->max_history = 5; config->sort_criteria = MAKO_SORT_CRITERIA_TIME; config->sort_asc = 0; } 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); } void init_default_style(struct mako_style *style) { style->width = 300; style->height = 100; style->outer_margin.top = 0; style->outer_margin.right = 0; style->outer_margin.bottom = 0; style->outer_margin.left = 0; 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_radius.top = 0; style->border_radius.right = 0; style->border_radius.bottom = 0; style->border_radius.left = 0; style->border_size = 2; #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->icon_border_radius = 0; style->font = strdup("monospace 10"); style->markup = true; style->format = strdup("%s\n%b"); style->text_alignment = PANGO_ALIGN_LEFT; 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; style->history = true; style->icon_location = MAKO_ICON_LOCATION_LEFT; style->output = strdup(""); style->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; style->max_visible = 5; style->anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; style->button_bindings.left.action = MAKO_BINDING_INVOKE_ACTION; style->button_bindings.left.action_name = strdup(DEFAULT_ACTION_KEY); style->button_bindings.right.action = MAKO_BINDING_DISMISS; style->button_bindings.middle.action = MAKO_BINDING_NONE; style->touch_binding.action = MAKO_BINDING_DISMISS; // 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)); } static void finish_binding(struct mako_binding *binding) { free(binding->command); free(binding->action_name); } void finish_style(struct mako_style *style) { finish_binding(&style->button_bindings.left); finish_binding(&style->button_bindings.middle); finish_binding(&style->button_bindings.right); finish_binding(&style->touch_binding); finish_binding(&style->notify_binding); free(style->icon_path); free(style->font); free(style->format); free(style->output); } static void copy_binding(struct mako_binding *dst, const struct mako_binding *src) { finish_binding(dst); *dst = *src; if (src->command != NULL) { dst->command = strdup(src->command); } if (src->action_name != NULL) { dst->action_name = strdup(src->action_name); } } // 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; char *new_output = 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; } } if (style->spec.output) { new_output = strdup(style->output); if (new_output == NULL) { free(new_format); free(new_font); free(new_icon_path); 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.outer_margin) { target->outer_margin = style->outer_margin; target->spec.outer_margin = 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.icon_border_radius) { target->icon_border_radius = style->icon_border_radius; target->spec.icon_border_radius = 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.text_alignment) { target->text_alignment = style->text_alignment; target->spec.text_alignment = 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->spec.history) { target->history = style->history; target->spec.history = true; } if (style->spec.icon_location) { target->icon_location = style->icon_location; target->spec.icon_location = true; } if (style->spec.border_radius) { target->border_radius = style->border_radius; target->spec.border_radius = true; } if (style->spec.output) { free(target->output); target->output = new_output; target->spec.output = true; } if (style->spec.anchor) { target->anchor = style->anchor; target->spec.anchor = true; } if (style->spec.layer) { target->layer = style->layer; target->spec.layer = true; } if (style->spec.max_visible) { target->max_visible = style->max_visible; target->spec.max_visible = true; } if (style->spec.button_bindings.left) { copy_binding(&target->button_bindings.left, &style->button_bindings.left); target->spec.button_bindings.left = true; } if (style->spec.button_bindings.middle) { copy_binding(&target->button_bindings.middle, &style->button_bindings.middle); target->spec.button_bindings.middle = true; } if (style->spec.button_bindings.right) { copy_binding(&target->button_bindings.right, &style->button_bindings.right); target->spec.button_bindings.right = true; } if (style->spec.touch_binding) { copy_binding(&target->touch_binding, &style->touch_binding); target->spec.touch_binding = true; } if (style->spec.notify_binding) { copy_binding(&target->notify_binding, &style->notify_binding); target->spec.notify_binding = 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.outer_margin = true; target->spec.margin = true; target->spec.padding = true; target->spec.border_size = true; target->spec.border_radius = 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.history = 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->outer_margin.top = max(style->outer_margin.top, target->outer_margin.top); target->outer_margin.right = max(style->outer_margin.right, target->outer_margin.right); target->outer_margin.bottom = max(style->outer_margin.bottom, target->outer_margin.bottom); target->outer_margin.left = max(style->outer_margin.left, target->outer_margin.left); 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_radius.top = max(style->border_radius.top, target->border_radius.top); target->border_radius.right = max(style->border_radius.right, target->border_radius.right); target->border_radius.bottom = max(style->border_radius.bottom, target->border_radius.bottom); target->border_radius.left = max(style->border_radius.left, target->border_radius.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; target->history |= style->history; // 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); target_format_pos += 2; // This needs to go to the next slot. } ++format_pos; // Enough to move to the next match. } } } return true; } static char *expand_config_path(const char *path) { if (strncmp(path, "/", 1) == 0) { return strdup(path); } if (strncmp(path, "~/", 2) != 0) { fprintf(stderr, "Config path must start with / or ~/\n"); return NULL; } const char *home = getenv("HOME"); if (home == NULL) { fprintf(stderr, "HOME env var not set\n"); return NULL; } return mako_asprintf("%s/%s", home, path + 2); } static bool apply_config_option(struct mako_config *config, const char *name, const char *value) { 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, "max-history") == 0) { return parse_int(value, &config->max_history); } else if (strcmp(name, "include") == 0) { char *path = expand_config_path(value); return path && load_config_file(config, path) == 0; } return false; } static bool has_prefix(const char *str, const char *prefix) { return strncmp(str, prefix, strlen(prefix)) == 0; } 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_ge(value, &style->width, 1); } else if (strcmp(name, "height") == 0) { return spec->height = parse_int_ge(value, &style->height, 1); } else if (strcmp(name, "outer-margin") == 0) { return spec->outer_margin = parse_directional(value, &style->outer_margin); } 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.left, style->padding.left); style->padding.right = max(style->border_radius.right, style->padding.right); } return spec->padding; } else if (strcmp(name, "border-size") == 0) { return spec->border_size = parse_int_ge(value, &style->border_size, 0); } 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, "icon-location") == 0) { if (!strcmp(value, "left")) { style->icon_location = MAKO_ICON_LOCATION_LEFT; } else if (!strcmp(value, "right")) { style->icon_location = MAKO_ICON_LOCATION_RIGHT; } else if (!strcmp(value, "top")) { style->icon_location = MAKO_ICON_LOCATION_TOP; } else if (!strcmp(value, "bottom")) { style->icon_location = MAKO_ICON_LOCATION_BOTTOM; } else { return false; } return spec->icon_location = true; } else if (strcmp(name, "max-icon-size") == 0) { return spec->max_icon_size = parse_int_ge(value, &style->max_icon_size, 1); } else if (strcmp(name, "icon-path") == 0) { free(style->icon_path); return spec->icon_path = !!(style->icon_path = strdup(value)); } else if (strcmp(name, "icon-border-radius") == 0) { spec->icon_border_radius = parse_int_ge(value, &style->icon_border_radius, 0); return spec->icon_border_radius; } 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, "text-alignment") == 0) { if (strcmp(value, "left") == 0) { style->text_alignment = PANGO_ALIGN_LEFT; } else if (strcmp(value, "center") == 0) { style->text_alignment = PANGO_ALIGN_CENTER; } else if (strcmp(value, "right") == 0) { style->text_alignment = PANGO_ALIGN_RIGHT; } else { return false; } style->spec.text_alignment = true; return true; } else if (strcmp(name, "default-timeout") == 0) { return spec->default_timeout = parse_int_ge(value, &style->default_timeout, 0); } 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, "history") == 0) { return spec->history = parse_boolean(value, &style->history); } else if (strcmp(name, "border-radius") == 0) { spec->border_radius = parse_directional(value, &style->border_radius); if (spec->border_radius && spec->padding) { style->padding.left = max(style->border_radius.left, style->padding.left); style->padding.right = max(style->border_radius.right, style->padding.right); } return spec->border_radius; } else if (strcmp(name, "max-visible") == 0) { return style->spec.max_visible = parse_int(value, &style->max_visible); } else if (strcmp(name, "output") == 0) { free(style->output); style->output = strdup(value); style->spec.output = true; return true; } else if (strcmp(name, "layer") == 0) { if (strcmp(value, "background") == 0) { style->layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; } else if (strcmp(value, "bottom") == 0) { style->layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; } else if (strcmp(value, "top") == 0) { style->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; } else if (strcmp(value, "overlay") == 0) { style->layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; } else { return false; } style->spec.layer = true; return true; } else if (strcmp(name, "anchor") == 0) { return spec->anchor = parse_anchor(value, &style->anchor); } else if (has_prefix(name, "on-")) { struct mako_binding binding = {0}; if (strcmp(value, "none") == 0) { binding.action = MAKO_BINDING_NONE; } else if (strcmp(value, "dismiss") == 0) { binding.action = MAKO_BINDING_DISMISS; } else if (strcmp(value, "dismiss --no-history") == 0) { binding.action = MAKO_BINDING_DISMISS_NO_HISTORY; } else if (strcmp(value, "dismiss-all") == 0) { binding.action = MAKO_BINDING_DISMISS_ALL; } else if (strcmp(value, "dismiss-group") == 0) { binding.action = MAKO_BINDING_DISMISS_GROUP; } else if (strcmp(value, "invoke-default-action") == 0) { binding.action = MAKO_BINDING_INVOKE_ACTION; binding.action_name = strdup(DEFAULT_ACTION_KEY); } else if (has_prefix(value, "invoke-action ")) { binding.action = MAKO_BINDING_INVOKE_ACTION; binding.action_name = strdup(value + strlen("invoke-action ")); } else if (has_prefix(value, "exec ")) { binding.action = MAKO_BINDING_EXEC; binding.command = strdup(value + strlen("exec ")); } else { return false; } if (strcmp(name, "on-button-left") == 0) { copy_binding(&style->button_bindings.left, &binding); style->spec.button_bindings.left = true; } else if (strcmp(name, "on-button-right") == 0) { copy_binding(&style->button_bindings.right, &binding); style->spec.button_bindings.right = true; } else if (strcmp(name, "on-button-middle") == 0) { copy_binding(&style->button_bindings.middle, &binding); style->spec.button_bindings.middle = true; } else if (strcmp(name, "on-touch") == 0) { copy_binding(&style->touch_binding, &binding); style->spec.touch_binding = true; } else if (strcmp(name, "on-notify") == 0) { copy_binding(&style->notify_binding, &binding); style->spec.notify_binding = true; } else { return false; } return true; } return false; } bool apply_global_option(struct mako_config *config, const char *name, const char *value) { struct mako_criteria *global = global_criteria(config); return apply_style_option(&global->style, name, value) || apply_config_option(config, name, value); } static bool file_exists(const char *path) { return path && access(path, R_OK) != -1; } static char *get_default_config_path() { const char *home = getenv("HOME"); if (home == NULL) { fprintf(stderr, "HOME env var not set\n"); return NULL; } const char *config_home = getenv("XDG_CONFIG_HOME"); char *config_home_fallback = NULL; if (config_home == NULL || config_home[0] == '\0') { config_home_fallback = mako_asprintf("%s/.config", home); config_home = config_home_fallback; } char *config_paths[] = { mako_asprintf("%s/.mako/config", home), mako_asprintf("%s/mako/config", config_home), }; size_t config_paths_len = sizeof(config_paths) / sizeof(config_paths[0]); char *found_path = NULL; for (size_t i = 0; i < config_paths_len; ++i) { char *path = config_paths[i]; if (file_exists(path)) { found_path = strdup(path); break; } } for (size_t i = 0; i < config_paths_len; ++i) { free(config_paths[i]); } free(config_home_fallback); return found_path; } int load_config_file(struct mako_config *config, char *path) { FILE *f = fopen(path, "r"); if (!f) { fprintf(stderr, "Unable to open %s for reading\n", 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; ssize_t len = 0; while ((len = getline(&line, &n, f)) != -1) { ++lineno; // We can't operate on `line` because we need to free it, so // make a copy char *stripped_line = line; // Strip leading whitespace while (stripped_line[0] == ' ' || stripped_line[0] == '\t') { ++stripped_line; --len; } if (stripped_line[0] == '\0' || stripped_line[0] == '\n' || stripped_line[0] == '#') { continue; } if (stripped_line[len - 1] == '\n') { stripped_line[--len] = '\0'; } // Strip trailing whitespace while (stripped_line[len - 1] == ' ' || stripped_line[len - 1] == '\t') { --len; } stripped_line[len] = '\0'; if (stripped_line[0] == '[' && stripped_line[len - 1] == ']') { // Since we hit the end of the previous criteria section, validate // that it doesn't break any rules before moving on. if (criteria != NULL && !validate_criteria(criteria)) { fprintf(stderr, "Invalid configuration in criteria: [%s]\n", criteria->raw_string); ret = -1; break; } free(section); section = strndup(stripped_line + 1, len - 2); 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(stripped_line, '='); if (!eq) { fprintf(stderr, "[%s:%d] Expected key=value\n", base, lineno); ret = -1; break; } bool valid_option = false; eq[0] = '\0'; valid_option = apply_style_option(&criteria->style, stripped_line, eq + 1); if (!valid_option && section == NULL) { valid_option = apply_config_option(config, stripped_line, eq + 1); } if (!valid_option) { eq[0] = '='; fprintf(stderr, "[%s:%d] Failed to parse option '%s'\n", base, lineno, stripped_line); ret = -1; break; } } // Validate the final criteria section since there was no opening bracket // after it to do this in the loop. if (ret != -1 && criteria != NULL && !validate_criteria(criteria)) { fprintf(stderr, "Invalid configuration in criteria: [%s]\n", criteria->raw_string); ret = -1; } 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'}, {"config", required_argument, 0, 'c'}, {"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}, {"outer-margin", 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-location", required_argument, 0, 0}, {"icon-path", required_argument, 0, 0}, {"max-icon-size", required_argument, 0, 0}, {"icon-border-radius", 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}, {"max-history", required_argument, 0, 0}, {"history", 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}, {"on-button-left", required_argument, 0, 0}, {"on-button-right", required_argument, 0, 0}, {"on-button-middle", required_argument, 0, 0}, {"on-touch", required_argument, 0, 0}, {0}, }; optind = 1; char *config_arg = NULL; int opt_status = 0; while (1) { int option_index = -1; int c = getopt_long(argc, argv, "hc:", long_options, &option_index); if (c < 0) { break; } else if (c == 'h') { opt_status = 1; break; } else if (c == 'c') { free(config_arg); config_arg = strdup(optarg); } else if (c != 0) { opt_status = -1; break; } } if (opt_status != 0) { free(config_arg); return opt_status; } char *config_path = config_arg ? config_arg : get_default_config_path(); if (config_path) { int config_status = load_config_file(config, config_path); if (config_status < 0) { return -1; } } optind = 1; while (1) { int option_index = -1; int c = getopt_long(argc, argv, "hc:", long_options, &option_index); if (c < 0) { break; } else if (c == 'h' || c == 'c') { continue; } else if (c != 0) { return -1; } const char *name = long_options[option_index].name; if (!apply_global_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 args_status = parse_config_arguments(&new_config, argc, argv); if (args_status > 0) { finish_config(&new_config); return args_status; } else if (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.10.0/contrib/000077500000000000000000000000001476555712500160165ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/completions/000077500000000000000000000000001476555712500203525ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/completions/bash/000077500000000000000000000000001476555712500212675ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/completions/bash/mako000066400000000000000000000032351476555712500221440ustar00rootroot00000000000000# mako(1) completion _mako() { local cur prev opts _get_comp_words_by_ref cur prev opts=( '--help' '-h' '--config' '-c' '--background-color' '--text-color' '--width' '--height' '--margin' '--padding' '--border-size' '--border-color' '--border-radius' '--progress-color' '--icons' '--icon-path' '--max-icon-size' '--icon-border-radius' '--markup' '--actions' '--format' '--hidden-format' '--max-visible' '--max-history' '--history' '--sort' '--default-timeout' '--ignore-timeout' '--output' '--layer' '--anchor' ) case $prev in -c|--config) COMPREPLY=($(compgen -f -- "$cur")) return ;; --icons|--markup|--actions|--history|--ignore-timeout) COMPREPLY=($(compgen -W "0 1" -- "$cur")) return ;; --output) local outputs outputs="$(swaymsg -t get_outputs 2>/dev/null | \ jq -r '.[] | select(.active) | "\(.name)\t\(.make) \(.model)"' 2>/dev/null)" COMPREPLY=($(compgen -W "$outputs" -- "$cur")) return ;; --layer) COMPREPLY=($(compgen -W "background bottom top overlay" -- "$cur")) return ;; --anchor) local pos pos=( 'top-right' 'top-center' 'top-left' 'bottom-right' 'bottom-center' 'bottom-left' 'center-right' 'center-left' 'center' ) COMPREPLY=($(compgen -W "${pos[*]}" -- "$cur")) return ;; esac if [[ "$prev" != -* ]]; then COMPREPLY=($(compgen -W "${opts[*]}" -- "$cur")) return fi } && complete -F _mako mako mako-notifier-1.10.0/contrib/completions/bash/makoctl000066400000000000000000000014301476555712500226420ustar00rootroot00000000000000# makoctl(1) completion _makoctl() { local cur prev cmds _get_comp_words_by_ref cur prev cmds=( 'dismiss' 'restore' 'invoke' 'menu' 'list' 'history' 'reload' 'set-mode' 'help' '-h' '--help' ) if [[ "$COMP_CWORD" == "1" ]]; then COMPREPLY=($(compgen -W "${cmds[*]}" -- "$cur")) return fi case $prev in dismiss) COMPREPLY=($(compgen -W "-a --all -g --group -n" -- "$cur")) return ;; invoke) COMPREPLY=($(compgen -W "-n" -- "$cur")) return ;; menu) COMPREPLY=($(compgen -c -W "-n" -- "$cur")) return ;; esac if [[ "${COMP_WORDS[COMP_CWORD-3]}" == "menu" ]]; then COMPREPLY=($(compgen -c -- "$cur")) return fi } && complete -F _makoctl makoctl mako-notifier-1.10.0/contrib/completions/fish/000077500000000000000000000000001476555712500213035ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/completions/fish/mako.fish000066400000000000000000000043201476555712500231040ustar00rootroot00000000000000function complete_outputs if string length -q "$SWAYSOCK"; and command -sq jq swaymsg -t get_outputs | jq -r '.[] | select(.active) | "\(.name)\t\(.make) \(.model)"' else return 1 end end complete -c mako -s h -l help -d 'Show help and exit' complete -c mako -s c -l config -d 'Path to config file' -r complete -c mako -l font -d 'Font family and size' -x complete -c mako -l background-color -d 'Background color in #RRGGBB[AA]' -x complete -c mako -l text-color -d 'Text color in #RRGGBB[AA]' -x complete -c mako -l width -d 'Notification width in px' -x complete -c mako -l height -d 'Max notification height in px' -x complete -c mako -l margin -d 'Margin values in px, comma separated' -x complete -c mako -l padding -d 'Padding values in px, comma separated' -x complete -c mako -l border-size -d 'Border size in px' -x complete -c mako -l border-color -d 'Border color in #RRGGBB[AA]' -x complete -c mako -l border-radius -d 'Border radius values in px,comma separated' -x complete -c mako -l progress-color -d 'Progress color indicator' -x complete -c mako -l icons -d 'Show icons or not' -xa "1 0" complete -c mako -l icon-path -d 'Icon search path, colon delimited' -r complete -c mako -l max-icon-size -d 'Max icon size in px' -x complete -c mako -l icon-border-radius -d 'Icon border radius value in px' -x complete -c mako -l markup -d 'Enable markup or not' -xa "1 0" complete -c mako -l actions -d 'Enable actions or not' -xa "1 0" complete -c mako -l format -d 'Format string' -x complete -c mako -l hidden-format -d 'Hidden format string' -x complete -c mako -l max-visible -d 'Max visible notifications' -x complete -c mako -l max-history -d 'Max size of history buffer' -x complete -c mako -l history -d 'Add expired notifications to history' -xa "1 0" complete -c mako -l sort -d 'Set notification sorting method' -x complete -c mako -l default-timeout -d 'Notification timeout in ms' -x complete -c mako -l ignore-timeout -d 'Enable notification timeout or not' -xa "1 0" complete -c mako -l output -d 'Show notifications on this output' -xa '(complete_outputs)' complete -c mako -l layer -d 'Show notifications on this layer' -x complete -c mako -l anchor -d 'Position on output to put notifications' -x mako-notifier-1.10.0/contrib/completions/fish/makoctl.fish000066400000000000000000000036271476555712500236200ustar00rootroot00000000000000function __fish_makoctl_complete_no_subcommand for i in (commandline -opc) if contains -- $i dismiss restore invoke menu list reload help return 1 end end return 0 end complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a dismiss -d 'Dismiss notification (the last one if none is given)' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a restore -d 'Restore the most recently expired notification from the history buffer' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a invoke -d 'Invoke an action on the notification (the last one if none is given)' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a menu -d 'Use a program to select one action to be invoked on the notification (the last one if none is given)' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a list -d 'List notifications' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a history -d 'List history' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a reload -d 'Reload the configuration file' -x complete -c makoctl -n '__fish_makoctl_complete_no_subcommand' -a help -d 'Show help message and quit' -x complete -c makoctl -n '__fish_seen_subcommand_from dismiss' -s a -l all -d "Dismiss all notifications" -x complete -c makoctl -n '__fish_seen_subcommand_from dismiss' -s g -l group -d "Dismiss all the notifications in the last notification's group" -x complete -c makoctl -n '__fish_seen_subcommand_from dismiss' -s n -d "Dismiss the notification with the given id" -x complete -c makoctl -n '__fish_seen_subcommand_from invoke' -s n -d "Invoke an action on the notification with the given id" -x complete -c makoctl -n '__fish_seen_subcommand_from menu' -s n -d "Use a program to select one action on the notification with the given id" -x complete -c makoctl -n '__fish_seen_subcommand_from menu' -a "(__fish_complete_command)" -x mako-notifier-1.10.0/contrib/completions/meson.build000066400000000000000000000020031476555712500225070ustar00rootroot00000000000000if get_option('zsh-completions') install_data( files( 'zsh/_makoctl', 'zsh/_mako', ), install_dir: get_option('datadir') / 'zsh/site-functions', install_mode: 'rw-r--r--', ) endif if get_option('fish-completions') fish_files = files('fish/mako.fish', 'fish/makoctl.fish') fish_comp = dependency('fish', required: false) if fish_comp.found() fish_install_dir = fish_comp.get_variable('completionsdir') else fish_install_dir = get_option('datadir') / 'fish/vendor_completions.d' endif install_data(fish_files, install_dir: fish_install_dir) endif if get_option('bash-completions') bash_files = files('bash/mako', 'bash/makoctl') bash_comp = dependency('bash-completion', required: false) if bash_comp.found() bash_install_dir = bash_comp.get_variable( pkgconfig: 'completionsdir', pkgconfig_define: ['datadir', get_option('datadir')] ) else bash_install_dir = get_option('datadir') / 'bash-completion/completions' endif install_data(bash_files, install_dir: bash_install_dir) endif mako-notifier-1.10.0/contrib/completions/zsh/000077500000000000000000000000001476555712500211565ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/completions/zsh/_mako000066400000000000000000000041771476555712500222000ustar00rootroot00000000000000#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:' \ '--border-radius[Corner radius values, comma separated. Up to four values, with the same meaning as in CSS.]:radius:' \ '--icon-border-radius[Icon corner radius value.]:radius:' \ '--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 notifications:' \ '--max-history[Max size of history buffer.]:historical notifications:' \ '--history[Add expired notification to history.]:history:' \ '--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 center-right center-left center)' \ '--sort[Sort incoming notifications by time and/or priority in ascending(+) or descending(-) order.]:sort pattern:' mako-notifier-1.10.0/contrib/completions/zsh/_makoctl000066400000000000000000000014551476555712500226770ustar00rootroot00000000000000#compdef makoctl local -a makoctl_cmds makoctl_cmds=( 'dismiss:Dismiss notification (first by default)' 'restore:Restore the most recently expired notification from the history buffer' 'invoke:Invoke an action on the first notification. If action is not specified, invoke the default action' 'list:Retrieve a list of current notifications' 'history:Retrieve a list of dismissed notifications' 'reload:Reload 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.10.0/contrib/systemd/000077500000000000000000000000001476555712500175065ustar00rootroot00000000000000mako-notifier-1.10.0/contrib/systemd/mako.service000066400000000000000000000005541476555712500220230ustar00rootroot00000000000000[Unit] Description=Lightweight Wayland notification daemon Documentation=man:mako(1) PartOf=graphical-session.target After=graphical-session.target [Service] Type=dbus BusName=org.freedesktop.Notifications ExecCondition=/bin/sh -c '[ -n "$WAYLAND_DISPLAY" ]' ExecStart=/usr/bin/mako ExecReload=/usr/bin/makoctl reload [Install] WantedBy=graphical-session.target mako-notifier-1.10.0/criteria.c000066400000000000000000000372221476555712500163320ustar00rootroot00000000000000#include #include #include #include #include #include #include #include "enum.h" #include "mako.h" #include "config.h" #include "criteria.h" #include "mode.h" #include "notification.h" #include "surface.h" #include "wayland.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); regfree(&criteria->summary_pattern); free(criteria->body); regfree(&criteria->body_pattern); free(criteria->raw_string); free(criteria->output); free(criteria->mode); free(criteria); } static bool match_regex_criteria(regex_t *pattern, char *value) { int ret = regexec(pattern, value, 0, NULL, 0); if (ret != 0) { if (ret != REG_NOMATCH) { size_t errlen = regerror(ret, pattern, NULL, 0); char errbuf[errlen]; regerror(ret, pattern, errbuf, sizeof(errbuf)); fprintf(stderr, "failed to match regex: %s\n", errbuf); } return false; } return true; } 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.hidden && criteria->hidden != notif->hidden) { 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.summary_pattern) { bool ret = match_regex_criteria(&criteria->summary_pattern, notif->summary); if (!ret) { return false; } } if (spec.body && strcmp(criteria->body, notif->body) != 0) { return false; } if (spec.body_pattern) { bool ret = match_regex_criteria(&criteria->body_pattern, notif->body); if (!ret) { 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; } if (spec.anchor && (notif->surface == NULL || criteria->anchor != notif->surface->anchor)) { return false; } if (spec.output && (notif->surface == NULL || notif->surface->surface_output == NULL || strcmp(criteria->output, notif->surface->surface_output->name) != 0)) { return false; } if (spec.mode && !has_mode(notif->state, criteria->mode)) { 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. abort(); } } 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; } // All user-specified criteria are implicitly unhidden by default. This // prevents any criteria sections that don't explicitly set `hidden` from // styling the hidden pseudo-notification. if (!criteria->spec.hidden) { criteria->hidden = false; criteria->spec.hidden = true; } 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) { criteria->summary = strdup(value); criteria->spec.summary = true; return true; } else if (strcmp(key, "summary~") == 0) { if (regcomp(&criteria->summary_pattern, value, REG_EXTENDED | REG_NOSUB)) { fprintf(stderr, "Invalid summary~ regex '%s'\n", value); return false; } criteria->spec.summary_pattern = true; return true; } else if (strcmp(key, "body") == 0) { criteria->body = strdup(value); criteria->spec.body = true; return true; } else if (strcmp(key, "body~") == 0) { if (regcomp(&criteria->body_pattern, value, REG_EXTENDED | REG_NOSUB)) { fprintf(stderr, "Invalid body~ regex '%s'\n", value); return false; } criteria->spec.body_pattern = true; return true; } else if (strcmp(key, "anchor") == 0) { return criteria->spec.anchor = parse_anchor(value, &criteria->anchor); } else if (strcmp(key, "output") == 0) { criteria->output = strdup(value); criteria->spec.output = true; return true; } else if (strcmp(key, "mode") == 0) { criteria->mode = strdup(value); criteria->spec.mode = true; return true; } else { // Anything left must be one of the boolean fields, defined using // standard syntax. Continue on. } } if (strcmp(key, "actionable") == 0) { if (!parse_boolean(value, &criteria->actionable)) { fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", value, key); return false; } criteria->spec.actionable = true; return true; } else if (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 (strcmp(key, "hidden") == 0) { if (!parse_boolean(value, &criteria->hidden)) { fprintf(stderr, "Invalid value '%s' for boolean field '%s'\n", value, key); return false; } criteria->spec.hidden = 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"); } // Retrieve 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; } } struct mako_surface *surface; wl_list_for_each(surface, ¬if->state->surfaces, link) { if (!strcmp(surface->configured_output, notif->style.output) && surface->anchor == notif->style.anchor && surface->layer == notif->style.layer) { notif->surface = surface; break; } } if (!notif->surface) { notif->surface = create_surface(notif->state, notif->style.output, notif->style.layer, notif->style.anchor); } 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; } wl_list_init(&criteria->link); 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); criteria->hidden = notif->hidden; return criteria; } // To keep the behavior of criteria predictable, there are a few rules that we // have to impose on what can be modified depending on what was matched. bool validate_criteria(struct mako_criteria *criteria) { char * invalid_option = NULL; if (criteria->spec.grouped || criteria->spec.group_index || criteria->spec.output || criteria->spec.anchor) { if (criteria->style.spec.anchor) { invalid_option = "anchor"; } else if (criteria->style.spec.output) { invalid_option = "output"; } else if (criteria->style.spec.group_criteria_spec) { invalid_option = "group-by"; } if (invalid_option) { fprintf(stderr, "Setting `%s` is not allowed when matching `grouped`, " "`group-index`, `output`, or `anchor`\n", invalid_option); return false; } } struct mako_criteria_spec copy = {0}; memcpy(©, &criteria->spec, sizeof(struct mako_criteria_spec)); copy.output = false; copy.anchor = false; copy.hidden = false; bool any_but_surface = mako_criteria_spec_any(©); if (criteria->style.max_visible && any_but_surface) { fprintf(stderr, "Setting `max_visible` is allowed only for `output` " "and/or `anchor`\n"); return false; } // Hidden is almost always specified, need to look at the actual value. if (criteria->hidden && any_but_surface) { fprintf(stderr, "Can only set `hidden` along with `output` " "and/or `anchor`\n"); return false; } if (criteria->spec.summary && criteria->spec.summary_pattern) { fprintf(stderr, "Cannot set both `summary` and `summary~`\n"); return false; } if (criteria->spec.body && criteria->spec.body_pattern) { fprintf(stderr, "Cannot set both `body` and `body~`\n"); return false; } if (criteria->style.spec.group_criteria_spec) { struct mako_criteria_spec *spec = &criteria->style.group_criteria_spec; if (spec->group_index) { invalid_option = "group-index"; } else if (spec->grouped) { invalid_option = "grouped"; } else if (spec->anchor) { invalid_option = "anchor"; } else if (spec->output) { invalid_option = "output"; } if (invalid_option) { fprintf(stderr, "`%s` cannot be used in `group-by`\n", invalid_option); return false; } } return true; } mako-notifier-1.10.0/dbus/000077500000000000000000000000001476555712500153135ustar00rootroot00000000000000mako-notifier-1.10.0/dbus/dbus.c000066400000000000000000000022361476555712500164170ustar00rootroot00000000000000#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.10.0/dbus/mako.c000066400000000000000000000265561476555712500164240ustar00rootroot00000000000000#include #include #include #include "config.h" #include "criteria.h" #include "surface.h" #include "dbus.h" #include "mako.h" #include "mode.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(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; uint32_t id = 0; int group = 0; int all = 0; int history = 1; // Keep history be default int ret = sd_bus_message_enter_container(msg, 'a', "{sv}"); if (ret < 0) { return ret; } while (true) { ret = sd_bus_message_enter_container(msg, 'e', "sv"); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char *key = NULL; ret = sd_bus_message_read(msg, "s", &key); if (ret < 0) { return ret; } if (strcmp(key, "id") == 0) { ret = sd_bus_message_read(msg, "v", "u", &id); } else if (strcmp(key, "group") == 0) { ret = sd_bus_message_read(msg, "v", "b", &group); } else if (strcmp(key, "history") == 0) { ret = sd_bus_message_read(msg, "v", "b", &history); } else if (strcmp(key, "all") == 0) { ret = sd_bus_message_read(msg, "v", "b", &all); } 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; } } // These don't make sense together if (all && group) { return -EINVAL; } else if ((all || group) && id != 0) { return -EINVAL; } struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (notif->id == id || id == 0) { struct mako_surface *surface = notif->surface; if (group) { close_group_notifications(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, history); } else if (all) { close_all_notifications(state, MAKO_NOTIFICATION_CLOSE_DISMISSED, history); } else { close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, history); } set_dirty(surface); break; } } 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; } struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (notif->id == id || id == 0) { struct mako_action *action; wl_list_for_each(action, ¬if->actions, link) { if (strcmp(action->key, action_key) == 0) { notify_action_invoked(action, NULL); break; } } break; } } return sd_bus_reply_method_return(msg, ""); } static int handle_restore_action(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; if (wl_list_empty(&state->history)) { goto done; } struct mako_notification *notif = wl_container_of(state->history.next, notif, link); wl_list_remove(¬if->link); insert_notification(state, notif); set_dirty(notif->surface); done: return sd_bus_reply_method_return(msg, ""); } static int handle_list(sd_bus_message *msg, struct wl_list *list) { 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, list, 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}", "category", "s", notif->category); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "{sv}", "desktop-entry", "s", notif->desktop_entry); 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_append(reply, "{sv}", "urgency", "y", notif->urgency); 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_list_notifications(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; return handle_list(msg, &state->notifications); } static int handle_list_history(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; return handle_list(msg, &state->history); } /** * The way surfaces are re-build here is not quite intuitive. * 1. All surfaces are destroyed. * 2. The styles and surface association of notifications is recomputed. * This will also (re)create all surfaces we need in the new config. * 3. Start the redraw events. */ static void reapply_config(struct mako_state *state) { struct mako_surface *surface, *tmp; wl_list_for_each_safe(surface, tmp, &state->surfaces, link) { destroy_surface(surface); } 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; /* Also reset the notif->surface so it gets reasigned to default * if appropriate */ notif->surface = NULL; 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); } wl_list_for_each(surface, &state->surfaces, link) { set_dirty(surface); } } static int handle_set_mode(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; const char *mode; int ret = sd_bus_message_read(msg, "s", &mode); if (ret < 0) { return ret; } set_modes(state, &mode, 1); reapply_config(state); return sd_bus_reply_method_return(msg, ""); } 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; } reapply_config(state); return sd_bus_reply_method_return(msg, ""); } static int handle_list_modes(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; } const char **mode_ptr; wl_array_for_each(mode_ptr, &state->current_modes) { ret = sd_bus_message_append_basic(reply, 's', *mode_ptr); 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_set_modes(sd_bus_message *msg, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; int ret = sd_bus_message_enter_container(msg, 'a', "s"); if (ret < 0) { return ret; } struct wl_array modes_arr; wl_array_init(&modes_arr); while (true) { const char *mode; ret = sd_bus_message_read(msg, "s", &mode); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char **dst = wl_array_add(&modes_arr, sizeof(char *)); *dst = mode; } ret = sd_bus_message_exit_container(msg); if (ret < 0) { return ret; } const char **modes = modes_arr.data; size_t modes_len = modes_arr.size / sizeof(char *); set_modes(state, modes, modes_len); wl_array_release(&modes_arr); reapply_config(state); return sd_bus_reply_method_return(msg, ""); } static int get_modes(sd_bus *bus, const char *path, const char *interface, const char *property, sd_bus_message *reply, void *data, sd_bus_error *ret_error) { struct mako_state *state = data; int ret = sd_bus_message_open_container(reply, 'a', "s"); if (ret < 0) { return ret; } const char **mode_ptr; wl_array_for_each(mode_ptr, &state->current_modes) { ret = sd_bus_message_append_basic(reply, 's', *mode_ptr); if (ret < 0) { return ret; } } ret = sd_bus_message_close_container(reply); if (ret < 0) { return ret; } return 0; } void emit_modes_changed(struct mako_state *state) { sd_bus_emit_properties_changed(state->bus, service_path, service_interface, "Modes", NULL); } static const sd_bus_vtable service_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_METHOD("DismissNotifications", "a{sv}", "", handle_dismiss, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("InvokeAction", "us", "", handle_invoke_action, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("RestoreNotification", "", "", handle_restore_action, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ListNotifications", "", "aa{sv}", handle_list_notifications, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ListHistory", "", "aa{sv}", handle_list_history, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("Reload", "", "", handle_reload, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("SetMode", "s", "", handle_set_mode, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("ListModes", "", "as", handle_list_modes, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_METHOD("SetModes", "as", "", handle_set_modes, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_PROPERTY("Modes", "as", get_modes, 0, SD_BUS_VTABLE_PROPERTY_EMITS_INVALIDATION), 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.10.0/dbus/xdg.c000066400000000000000000000335141476555712500162470ustar00rootroot00000000000000#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_append(reply, "s", "x-canonical-private-synchronous"); if (ret < 0) { return ret; } ret = sd_bus_message_append(reply, "s", "x-dunst-stack-tag"); 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; struct mako_surface *surface = notif->surface; notif->timer = NULL; close_notification(notif, MAKO_NOTIFICATION_CLOSE_EXPIRED, true); set_dirty(surface); } 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; } free(notif->app_name); free(notif->app_icon); free(notif->summary); free(notif->body); notif->app_name = strdup(app_name); notif->app_icon = strdup(app_icon); notif->summary = strdup(summary); notif->body = strdup(body); 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 if (strcmp(contents, "y") == 0) { uint8_t urgency = 0; ret = sd_bus_message_read(msg, "v", "y", &urgency); if (ret < 0) { return ret; } notif->urgency = urgency; } else if (strcmp(contents, "i") == 0) { int32_t urgency = 0; ret = sd_bus_message_read(msg, "v", "i", &urgency); if (ret < 0) { return ret; } notif->urgency = urgency; } else { fprintf(stderr, "Unsupported variant type for \"urgency\": \"%s\"\n", contents); return -1; } } 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-path") == 0 || strcmp(hint, "image_path") == 0) { // Deprecated. const char *image_path = NULL; ret = sd_bus_message_read(msg, "v", "s", &image_path); if (ret < 0) { return ret; } // image-path is higher priority than app_icon, so just overwrite // it. We're guaranteed to be doing this after reading the "real" // app_icon. It's also lower priority than image-data, and that // will win over app_icon if provided. free(notif->app_icon); notif->app_icon = strdup(image_path); } else if (strcmp(hint, "x-canonical-private-synchronous") == 0 || strcmp(hint, "x-dunst-stack-tag") == 0) { const char *tag = NULL; ret = sd_bus_message_read(msg, "v", "s", &tag); if (ret < 0) { return ret; } notif->tag = strdup(tag); } else if (strcmp(hint, "image-data") == 0 || strcmp(hint, "image_data") == 0 || // Deprecated. strcmp(hint, "icon_data") == 0) { // Even more deprecated. 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; if (notif->tag) { // Find and replace the existing notfication with a matching tag struct mako_notification *replace_notif = get_tagged_notification(state, notif->tag, app_name); if (replace_notif) { notif->id = replace_notif->id; wl_list_insert(&replace_notif->link, ¬if->link); destroy_notification(replace_notif); replaces_id = notif->id; } } // 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 notifications 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 criteria 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); destroy_criteria(notif_criteria); notification_execute_binding(notif, ¬if->style.notify_binding, NULL); set_dirty(notif->surface); 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) { struct mako_surface *surface = notif->surface; close_notification(notif, MAKO_NOTIFICATION_CLOSE_REQUEST, true); set_dirty(surface); } 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, const char *activation_token) { if (!action->notification->style.actions) { // Actions are disabled for this notification, bail. return; } struct mako_state *state = action->notification->state; if (activation_token != NULL) { sd_bus_emit_signal(state->bus, service_path, service_interface, "ActivationToken", "us", action->notification->id, activation_token); } sd_bus_emit_signal(state->bus, service_path, service_interface, "ActionInvoked", "us", action->notification->id, action->key); } mako-notifier-1.10.0/doc/000077500000000000000000000000001476555712500151235ustar00rootroot00000000000000mako-notifier-1.10.0/doc/mako.1.scd000066400000000000000000000014671476555712500167140ustar00rootroot00000000000000mako(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 D-Bus are displayed until dismissed with a click or via *makoctl*(1). # OPTIONS *-h, --help* Show help message and quit. *-c, --config* Custom path to the config file. Additionally, global configuration options can be specified. Passing *--key=value* is equivalent to a *key=value* line in the configuration file. See *mako*(5) for a list of options. # 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*(5) *makoctl*(1) mako-notifier-1.10.0/doc/mako.5.scd000066400000000000000000000347241476555712500167220ustar00rootroot00000000000000mako(5) # NAME mako - configuration file # DESCRIPTION The config file is located at *~/.config/mako/config* or at *$XDG\_CONFIG\_HOME/mako/config*. Option lines can be specified to configure mako like so: key=value Empty lines and lines that begin with # are ignored. # GLOBAL CONFIGURATION OPTIONS *max-history*=_n_ Set maximum number of expired notifications to keep in the history buffer to _n_. If the buffer is full, newly expired notifications replace the oldest ones. If 0, history is disabled. Default: 5 *sort*=_+/-time_ | _+/-priority_ Sorts incoming notifications by time and/or priority in ascending(+) or descending(-) order. Default: -time *include*=_config path_ Includes a config at the specified path. The path must be absolute or otherwise start with ~/. Default: none # BINDING OPTIONS Bindings allow one to perform an action when an event is triggered. Supported options: *on-button-left*=_action_ Performs the action when the left pointer button is pressed. Default: invoke-default-action *on-button-middle*=_action_ Performs the action when the middle pointer button is pressed. Default: none *on-button-right*=_action_ Performs the action when the right pointer button is pressed. Default: dismiss *on-touch*=_action_ Performs the action when tapped via a touch device. Default: dismiss *on-notify*=_action_ Performs the action when the notification is opened. Default: none Supported actions: *none* Do nothing. *dismiss [--no-history]* Dismiss the current notification. If *--no-history* is passed, the notification won't be added to history. *dismiss-all* Dismiss all notifications. *dismiss-group* Dismiss notifications in the current group. *invoke-action* Invoke the named action on the notification. An xdg-activation token will be sent to the client, allowing the client to request focus from the compositor. The compositor must support the xdg_activation_v1 protocol and allow the focus request for the following example to work correctly: ``` [app-name="some-app-id" actionable] on-button-left=invoke-action mail-reply-sender ``` *invoke-default-action* Invoke the default action on the notification. *exec* Execute a shell command. The command will be executed in a POSIX shell. The shell variable _id_ will be set to the notification ID. For example, the following option will display an interactive action menu on middle click: ``` on-button-middle=exec makoctl menu -n "$id" dmenu -p 'Select action: ' ``` The following option will play a sound when a new notification is opened: ``` on-notify=exec mpv /usr/share/sounds/freedesktop/stereo/message.oga ``` # STYLE OPTIONS *font*=_font_ Set font to _font_, as a Pango font description. For more information on Pango font descriptions, see: https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html#description 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 maximum height of notification popups. Notifications whose text takes up less space are shrunk to fit. Default: 100 *outer-margin*=_directional_ Set outer-margin of each edge to the size specified by _directional_. See *DIRECTIONAL VALUES* for more information. This margin applies to the outside of the whole notification list. Default: 0 *margin*=_directional_ Set margin of each edge to the size specified by _directional_. See *DIRECTIONAL VALUES* for more information. This margin applies to each individual notification. Note that it applies in addition to outer-margin, meaning first and last notifications will use the sum of both margins. 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: 2 *border-color*=_color_ Set popup border color to _color_. See *COLORS* for more information. Default: #4C7899FF *border-radius*=_directional_ Set popup corner radius on each side to the size specified by _directional_. See *DIRECTIONAL VALUES* for more information. 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). Progress can be indicated in a notification by setting a hint, "value" to an integer between 0 and 100 inclusive. 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. The path should be the root of the icon theme, the categories and resolutions will be searched for the most appropriate match. /usr/share/icons/hicolor and /usr/share/pixmaps are always searched. Default: "" *icon-location*=_position_ Position of the icon relative to the displayed text. Valid options are _left_, _right_, _top_ and _bottom_. Default: left *icon-border-radius*=_px_ Sets icon corner radius to _px_ pixels. Default: 0 *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 *history*=0|1 If set, mako will save notifications that have reached their timeout into the history buffer instead of immediately deleting them. _max-history_ determines the size of the history buffer. 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 *text-alignment*=left|center|right Set notification text alignment. Default: left *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 *max-visible*=_n_ Set maximum number of visible notifications to _n_. Older notifications will be hidden. If -1, all notifications are visible. Default: 5 *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_, _center-right_, _center-left_ and _center_. Default: top-right # 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 *BINDING OPTIONS* and *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): exact match on the summary of the notification. This field conflicts with _summary~_. - _summary~_ (string): a POSIX extended regular expression match on the summary of the notification. This field conflicts with _summary_. - _body_ (string): an exact match on the body of the notification. This field conflicts with _body~_. - _body~_ (string): a POSIX extended regular expression match on the body of the notification. This field conflicts with _body_. - _urgency_ (one of "low", "normal", "critical") - _category_ (string) - _desktop-entry_ (string) - _actionable_ (boolean) - _expiring_ (boolean) - _mode_ (string): only apply style options in this section if the provided mode is currently enabled. For more information about modes, see the _MODES_ section. The following fields are also available to match on a second pass based on where previous style options decided to place each notification: - _grouped_ (boolean): whether the notification is grouped with any others. - _group-index_ (int): the notification's index within its group, or -1 if it is not grouped. - _hidden_ (boolean): special field which defines the style for the placeholder shown when the number of notifications or groups exceeds _max-visible_. - _output_ (string): which output the notification was sorted onto. See the _output_ style option for possible values. - _anchor_ (string): which position on the output the notification was assigned to. See the _anchor_ style option for possible values. There are only two passes performed on each notification, so the second-pass criteria are not allowed to reposition the notification. 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. There are some rules restricting what can be configured depending on what is being matched by a given criteria. Criteria matching _grouped_ or _group-index_ are not allowed to change the _anchor_, _output_, or _group-by_, as this would invalidate the grouping. Grouping is only performed once rather than recursively, to avoid the potential for infinite loops. # 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 # 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 set top, right, bottom, and left edges separately. 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 *%i* Notification id ## For the hidden notifications placeholder *%h* Number of hidden notifications *%t* Total number of notifications # MODES mako supports applying style options conditionally via modes. A configuration section with a _mode_ criteria will only be applied if the current mode matches. **makoctl**(1) can be used to change the current mode. The initial list of modes contains a single mode called "default". This is deprecated, in a future version the initial list of modes will be empty. For example, to hide all notifications if the mode "do-not-disturb" is enabled: ``` [mode=do-not-disturb] invisible=1 ``` _makoctl mode -a do-not-disturb_ will hide all notifications, _makoctl mode -r do-not-disturb_ will show them again. # 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) *makoctl*(1) mako-notifier-1.10.0/doc/makoctl.1.scd000066400000000000000000000046321476555712500174140ustar00rootroot00000000000000makoctl(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* [-a|--all] [-g|--group] [-n ] Dismisses a notification. Options: *-a, --all* Dismiss all notifications. *-g, --group* Dismiss the first notification group. *-h, --no-history* Dismiss the current notification without adding it to history. *-n* Dismiss the notification with the given id. Defaults to the first notification. *restore* Restores the most recently expired notification from the history buffer. *invoke* [-n ] [action] Invokes an action on a notification. If _action_ is not specified, invokes the default action. Action names can be discovered using `makoctl list`. Options: *-n* Invoke the action on the notification with the given id. Defaults to the first notification. *menu* [-n ] [argument...] Use a program to select an action on a notification. The list of actions are joined on newlines and passed to _program_. The program should write the selected action to stdout. If an action is given, this action will be invoked. If no action is found, or no action is selected, _makoctl_ will return non-zero. Options: *-n* List the actions of the notification with the given id. Defaults to the first notification. Examples: ``` makoctl menu dmenu -p 'Select Action: ' makoctl menu -n 12345 wofi -d -p 'Choose Action: ' ``` *list* Retrieve a list of current notifications. *history* Retrieve a list of dismissed notifications. *reload* Reloads the configuration file. *mode* ++ *mode* -s ... ++ *mode* [-a mode]... [-r mode]... [-t mode]... +++ When run without any option, retrieves a list of current modes. When run with the _-s_ option, replaces the current modes with the provided list. When run with the _-a_ or _-r_ options, adds or removes the provided mode from the current modes. When run with the _-t_ option, toggle the mode by removing it when present and otherwise adding it. See the _MODES_ section in **mako**(5) for more information about modes. *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.10.0/doc/meson.build000066400000000000000000000010661476555712500172700ustar00rootroot00000000000000scdoc = dependency('scdoc', required: get_option('man-pages'), version: '>= 1.9.7', native: true) if not scdoc.found() subdir_done() endif man_pages = ['mako.1.scd', 'mako.5.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: scdoc.get_variable('scdoc'), feed: true, capture: true, install: true, install_dir: '@0@/man@1@'.format(mandir, section) ) endforeach mako-notifier-1.10.0/event-loop.c000066400000000000000000000141571476555712500166220ustar00rootroot00000000000000#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; // Unprocessed messages can be queued up by synchronous sd_bus methods. We // need to process these. do { ret = sd_bus_process(loop->bus, NULL); } while (ret > 0); if (ret < 0) { return ret; } while (loop->running) { errno = 0; // 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); 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); } } return ret; } mako-notifier-1.10.0/fr.emersion.mako.service.in000066400000000000000000000001061476555712500215170ustar00rootroot00000000000000[D-BUS Service] Name=org.freedesktop.Notifications Exec=@bindir@/mako mako-notifier-1.10.0/icon.c000066400000000000000000000205641476555712500154610ustar00rootroot00000000000000#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 bool validate_icon_name(const char* icon_name) { int icon_len = strlen(icon_name); if (icon_len > 1024) { return false; } int index; for (index = 0; index < icon_len; index ++) { bool is_number = icon_name[index] >= '0' && icon_name[index] <= '9'; bool is_abc = (icon_name[index] >= 'A' && icon_name[index] <= 'Z') || (icon_name[index] >= 'a' && icon_name[index] <= 'z'); bool is_other = icon_name[index] == '-' || icon_name[index] == '.' || icon_name[index] == '_'; bool is_legal = is_number || is_abc || is_other; if (!is_legal) { return false; } } return true; } 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; if (!validate_icon_name(icon_name)) { return NULL; } 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); g_object_unref(image); if (icon->image == NULL) { free(icon); return NULL; } 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.10.0/include/000077500000000000000000000000001476555712500160015ustar00rootroot00000000000000mako-notifier-1.10.0/include/cairo-pixbuf.h000066400000000000000000000004111476555712500205360ustar00rootroot00000000000000#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.10.0/include/config.h000066400000000000000000000065151476555712500174260ustar00rootroot00000000000000#ifndef MAKO_CONFIG_H #define MAKO_CONFIG_H #include #include #include #include #include "types.h" enum mako_binding_action { MAKO_BINDING_NONE, MAKO_BINDING_DISMISS, MAKO_BINDING_DISMISS_NO_HISTORY, MAKO_BINDING_DISMISS_GROUP, MAKO_BINDING_DISMISS_ALL, MAKO_BINDING_INVOKE_ACTION, MAKO_BINDING_EXEC, }; struct mako_binding { enum mako_binding_action action; char *command; // for MAKO_BINDING_EXEC char *action_name; // for MAKO_BINDING_INVOKE_ACTION }; enum mako_sort_criteria { MAKO_SORT_CRITERIA_TIME = 1, MAKO_SORT_CRITERIA_URGENCY = 2, }; enum mako_icon_location { MAKO_ICON_LOCATION_LEFT, MAKO_ICON_LOCATION_RIGHT, MAKO_ICON_LOCATION_TOP, MAKO_ICON_LOCATION_BOTTOM, }; // 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, outer_margin, margin, padding, border_size, border_radius, font, markup, format, text_alignment, actions, default_timeout, ignore_timeout, icons, max_icon_size, icon_path, icon_border_radius, group_criteria_spec, invisible, history, icon_location, max_visible, layer, output, anchor; struct { bool background, text, border, progress; } colors; struct { bool left, right, middle; } button_bindings; bool touch_binding, notify_binding; }; struct mako_style { struct mako_style_spec spec; int32_t width; int32_t height; struct mako_directional outer_margin; struct mako_directional margin; struct mako_directional padding; struct mako_directional border_radius; int32_t border_size; bool icons; int32_t max_icon_size; char *icon_path; int32_t icon_border_radius; char *font; bool markup; char *format; PangoAlignment text_alignment; 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 bool history; enum mako_icon_location icon_location; int32_t max_visible; char *output; enum zwlr_layer_shell_v1_layer layer; uint32_t anchor; struct { struct mako_binding left, right, middle; } button_bindings; struct mako_binding touch_binding, notify_binding; }; struct mako_config { struct wl_list criteria; // mako_criteria::link uint32_t sort_criteria; //enum mako_sort_criteria uint32_t sort_asc; int32_t max_history; struct mako_style superstyle; }; #define DEFAULT_ACTION_KEY "default" 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, char *config_arg); int reload_config(struct mako_config *config, int argc, char **argv); bool apply_global_option(struct mako_config *config, const char *name, const char *value); #endif mako-notifier-1.10.0/include/criteria.h000066400000000000000000000031761476555712500177630ustar00rootroot00000000000000#ifndef MAKO_CRITERIA_H #define MAKO_CRITERIA_H #include #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; regex_t summary_pattern; char *body; regex_t body_pattern; char *mode; // Second-pass matches: int group_index; bool grouped; // Whether group_index is non-zero char *output; uint32_t anchor; bool hidden; }; 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); bool validate_criteria(struct mako_criteria *criteria); #endif mako-notifier-1.10.0/include/dbus.h000066400000000000000000000014111476555712500171040ustar00rootroot00000000000000#ifndef MAKO_DBUS_H #define MAKO_DBUS_H #include #if defined(HAVE_LIBSYSTEMD) #include #elif defined(HAVE_LIBELOGIND) #include #elif defined(HAVE_BASU) #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, const char *activation_token); int init_dbus_xdg(struct mako_state *state); void emit_modes_changed(struct mako_state *state); int init_dbus_mako(struct mako_state *state); #endif mako-notifier-1.10.0/include/enum.h000066400000000000000000000005451476555712500171220ustar00rootroot00000000000000#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.10.0/include/event-loop.h000066400000000000000000000024061476555712500202440ustar00rootroot00000000000000#ifndef MAKO_EVENT_LOOP_H #define MAKO_EVENT_LOOP_H #include #include #include #include #if defined(HAVE_LIBSYSTEMD) #include #elif defined(HAVE_LIBELOGIND) #include #elif defined(HAVE_BASU) #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.10.0/include/icon.h000066400000000000000000000011141476555712500170770ustar00rootroot00000000000000#ifndef MAKO_ICON_H #define MAKO_ICON_H #include #include "notification.h" struct mako_icon { double width; double height; double scale; int32_t border_radius; 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.10.0/include/mako.h000066400000000000000000000036241476555712500171060ustar00rootroot00000000000000#ifndef MAKO_H #define MAKO_H #include #include #include #if defined(HAVE_LIBSYSTEMD) #include #elif defined(HAVE_LIBELOGIND) #include #elif defined(HAVE_BASU) #include #endif #include "config.h" #include "event-loop.h" #include "pool-buffer.h" #include "cursor-shape-v1-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "xdg-activation-v1-client-protocol.h" struct mako_state; struct mako_surface { struct wl_list link; struct mako_state *state; struct wl_surface *surface; struct mako_output *surface_output; struct zwlr_layer_surface_v1 *layer_surface; struct mako_output *layer_surface_output; struct wl_callback *frame_callback; bool configured; bool dirty; // Do we need to redraw? int32_t scale; char *configured_output; enum zwlr_layer_shell_v1_layer layer; uint32_t anchor; int32_t width, height; struct pool_buffer buffers[2]; struct pool_buffer *current_buffer; }; 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 xdg_activation_v1 *xdg_activation; struct wp_cursor_shape_manager_v1 *cursor_shape_manager; struct wl_list outputs; // mako_output::link struct wl_list seats; // mako_seat::link struct { uint32_t size; uint32_t scale; struct wl_cursor_theme *theme; const struct wl_cursor_image *image; struct wl_surface *surface; } cursor; struct wl_list surfaces; // mako_surface::link uint32_t last_id; struct wl_list notifications; // mako_notification::link struct wl_list history; // mako_notification::link struct wl_array current_modes; // char * int argc; char **argv; }; #endif mako-notifier-1.10.0/include/mode.h000066400000000000000000000003341476555712500170760ustar00rootroot00000000000000#ifndef MODE_H #define MODE_H #include struct mako_state; bool has_mode(struct mako_state *state, const char *mode); void set_modes(struct mako_state *state, const char **modes, size_t modes_len); #endif mako-notifier-1.10.0/include/notification.h000066400000000000000000000064001476555712500206400ustar00rootroot00000000000000#ifndef MAKO_NOTIFICATION_H #define MAKO_NOTIFICATION_H #include #include #include #include "config.h" #include "types.h" struct mako_state; struct mako_surface; 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 mako_surface *surface; struct wl_list link; // mako_state::notifications struct mako_style style; struct mako_icon *icon; uint32_t id; int group_index; int group_count; bool hidden; 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; char *tag; 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; }; struct mako_binding_context { struct mako_surface *surface; struct mako_seat *seat; uint32_t serial; }; 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, bool add_to_history); void close_group_notifications(struct mako_notification *notif, enum mako_notification_close_reason reason, bool add_to_history); void close_all_notifications(struct mako_state *state, enum mako_notification_close_reason reason, bool add_to_history); 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); struct mako_notification *get_tagged_notification(struct mako_state *state, const char *tag, const char *app_name); 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, const struct mako_binding_context *ctx); void notification_handle_touch(struct mako_notification *notif, const struct mako_binding_context *ctx); void notification_execute_binding(struct mako_notification *notif, const struct mako_binding *binding, const struct mako_binding_context *ctx); 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.10.0/include/pool-buffer.h000066400000000000000000000010401476555712500203650ustar00rootroot00000000000000#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.10.0/include/render.h000066400000000000000000000003121476555712500174250ustar00rootroot00000000000000#ifndef MAKO_RENDER_H #define MAKO_RENDER_H struct mako_state; struct mako_surface; void render(struct mako_surface *surface, struct pool_buffer *buffer, int scale, int *width, int *height); #endif mako-notifier-1.10.0/include/string-util.h000066400000000000000000000001401476555712500204260ustar00rootroot00000000000000#ifndef MAKO_STRING_H #define MAKO_STRING_H char *mako_asprintf(const char *fmt, ...); #endif mako-notifier-1.10.0/include/surface.h000066400000000000000000000004641476555712500176060ustar00rootroot00000000000000#ifndef MAKO_SURFACE_H #define MAKO_SURFACE_H #include "config.h" struct mako_state; struct mako_surface; void destroy_surface(struct mako_surface *surface); struct mako_surface *create_surface(struct mako_state *state, const char *output, enum zwlr_layer_shell_v1_layer layer, uint32_t anchor); #endif mako-notifier-1.10.0/include/types.h000066400000000000000000000041461476555712500173230ustar00rootroot00000000000000#ifndef MAKO_TYPES_H #define MAKO_TYPES_H #include #include #include #include "wlr-layer-shell-unstable-v1-client-protocol.h" 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_int_ge(const char *string, int *out, int min); bool parse_color(const char *string, uint32_t *out); bool parse_mako_color(const char *string, struct mako_color *out); bool parse_anchor(const char *string, uint32_t *out); enum mako_notification_urgency { MAKO_NOTIFICATION_URGENCY_LOW = 0, MAKO_NOTIFICATION_URGENCY_NORMAL = 1, MAKO_NOTIFICATION_URGENCY_CRITICAL = 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 summary_pattern; bool body; bool body_pattern; bool mode; bool none; // Special criteria that never matches, used for grouping // Fields that can only be matched after grouping, and thus can't be // used to group. bool group_index; bool grouped; bool output; bool anchor; bool hidden; }; bool parse_criteria_spec(const char *string, struct mako_criteria_spec *out); bool mako_criteria_spec_any(const struct mako_criteria_spec *spec); // 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.10.0/include/wayland.h000066400000000000000000000017531476555712500176170ustar00rootroot00000000000000#ifndef MAKO_WAYLAND_H #define MAKO_WAYLAND_H #include #include #define MAX_TOUCHPOINTS 10 struct mako_state; struct mako_surface; struct mako_output { struct mako_state *state; uint32_t global_name; struct wl_output *wl_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; struct mako_surface *surface; } pointer; struct { struct wl_touch *wl_touch; struct { int32_t x, y; struct mako_surface *surface; } pts[MAX_TOUCHPOINTS]; } touch; }; bool init_wayland(struct mako_state *state); void finish_wayland(struct mako_state *state); void set_dirty(struct mako_surface *surface); char *create_xdg_activation_token(struct mako_surface *surface, struct mako_seat *seat, uint32_t serial); #endif mako-notifier-1.10.0/main.c000066400000000000000000000121301476555712500154430ustar00rootroot00000000000000#include #include #include #include "config.h" #include "dbus.h" #include "mako.h" #include "mode.h" #include "notification.h" #include "render.h" #include "surface.h" #include "wayland.h" static const char usage[] = "Usage: mako [options...]\n" "\n" " -h, --help Show help message and quit.\n" " -c, --config Path to config file.\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" " --outer-margin [,...] Outer margin values, comma separated.\n" " Up to four values, with the same\n" " meaning as in CSS.\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, comma separated.\n" " Up to four values, with the same\n" " meaning as in CSS.\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" " --icon-border-radius Icon's corner radius.\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" " --max-history Max size of history buffer.\n" " --history <0|1> Add expired notifications to history.\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); wl_list_init(&state->history); wl_array_init(&state->current_modes); const char *mode = "default"; set_modes(state, &mode, 1); return true; } static void finish(struct mako_state *state) { char **mode_ptr; wl_array_for_each(mode_ptr, &state->current_modes) { free(*mode_ptr); } wl_array_release(&state->current_modes); struct mako_notification *notif, *tmp; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { destroy_notification(notif); } wl_list_for_each_safe(notif, tmp, &state->history, link) { destroy_notification(notif); } struct mako_surface *surface, *stmp; wl_list_for_each_safe(surface, stmp, &state->surfaces, link) { destroy_surface(surface); } 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; wl_list_init(&state.surfaces); // 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) { finish_config(&state.config); return EXIT_FAILURE; } else if (ret > 0) { finish_config(&state.config); 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.10.0/makoctl.c000066400000000000000000000462311476555712500161620ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #if defined(HAVE_LIBSYSTEMD) #include #elif defined(HAVE_LIBELOGIND) #include #elif defined(HAVE_BASU) #include #endif static void log_neg_errno(int ret, const char *msg, ...) { va_list args; va_start(args, msg); vfprintf(stderr, msg, args); va_end(args); fprintf(stderr, ": %s\n", strerror(-ret)); } static int parse_uint32(uint32_t *out, const char *str) { char *end; errno = 0; uintmax_t u = strtoumax(str, &end, 10); if (errno != 0) { return -errno; } else if (end[0] != '\0') { return -EINVAL; } else if (u > UINT32_MAX) { return -ERANGE; } *out = (uint32_t)u; return 0; } static int new_method_call(sd_bus *bus, sd_bus_message **m, const char *member) { int ret = sd_bus_message_new_method_call(bus, m, "org.freedesktop.Notifications", "/fr/emersion/Mako", "fr.emersion.Mako", member); if (ret < 0) { log_neg_errno(ret, "sd_bus_message_new_method_call() failed for %s", member); } return ret; } static int call(sd_bus *bus, sd_bus_message *m, sd_bus_message **reply) { sd_bus_error error = {0}; int ret = sd_bus_call(bus, m, 0, &error, reply); if (ret < 0) { fprintf(stderr, "%s (%s)\n", error.message, error.name); sd_bus_error_free(&error); } return ret; } static int call_method(sd_bus *bus, const char *member, sd_bus_message **reply, const char *types, ...) { sd_bus_message *m = NULL; int ret = new_method_call(bus, &m, member); if (ret < 0) { return ret; } va_list args; va_start(args, types); ret = sd_bus_message_appendv(m, types, args); va_end(args); if (ret < 0) { log_neg_errno(ret, "sd_bus_message_appendv() failed for %s", member); return ret; } ret = call(bus, m, reply); sd_bus_message_unref(m); return ret; } static int run_dismiss(sd_bus *bus, int argc, char *argv[]) { uint32_t id = 0; bool group = false; bool all = false; bool no_history = false; while (true) { const struct option options[] = { { "all", no_argument, 0, 'a' }, { "group", no_argument, 0, 'g' }, { "no-history", no_argument, 0, 'h' }, {0}, }; int opt = getopt_long(argc, argv, "aghn:", options, NULL); if (opt == -1) { break; } switch (opt) { case 'a': all = true; break; case 'g': group = true; break; case 'n':; int ret = parse_uint32(&id, optarg); if (ret < 0) { log_neg_errno(ret, "invalid notification ID"); return 1; } break; case 'h':; no_history = true; break; default: return -EINVAL; } } if (all && group) { fprintf(stderr, "-a and -g cannot be used together\n"); return -EINVAL; } else if ((all || group) && id != 0) { fprintf(stderr, "-n cannot be used with -a or -g\n"); return -EINVAL; } char types[6] = "a{sv}"; sd_bus_message *msg = NULL; int ret = new_method_call(bus, &msg, "DismissNotifications"); if (ret < 0) { return ret; } ret = sd_bus_message_open_container(msg, 'a', "{sv}"); if (ret < 0) { return ret; } ret = sd_bus_message_append(msg, "{sv}", "id", "u", id); if (ret < 0) { return ret; } ret = sd_bus_message_append(msg, "{sv}", "group", "b", (int)group); if (ret < 0) { return ret; } int history = !no_history; ret = sd_bus_message_append(msg, "{sv}", "history", "b", (int)history); if (ret < 0) { return ret; } ret = sd_bus_message_append(msg, "{sv}", "all", "b", (int)all); if (ret < 0) { return ret; } ret = sd_bus_message_close_container(msg); if (ret < 0) { return ret; } ret = call(bus, msg, NULL); sd_bus_message_unref(msg); return ret; return call_method(bus, "DismissNotifications", NULL, types, &msg); } static int run_invoke(sd_bus *bus, int argc, char *argv[]) { uint32_t id = 0; while (true) { int opt = getopt(argc, argv, "n:"); if (opt == -1) { break; } switch (opt) { case 'n':; int ret = parse_uint32(&id, optarg); if (ret < 0) { log_neg_errno(ret, "invalid notification ID"); return 1; } break; default: return -EINVAL; } } const char *action = "default"; if (optind < argc) { action = argv[optind]; } return call_method(bus, "InvokeAction", NULL, "us", id, action); } static int read_actions(sd_bus_message *msg, char ***out) { int ret = sd_bus_message_enter_container(msg, 'v', "a{ss}"); if (ret < 0) { return ret; } ret = sd_bus_message_enter_container(msg, 'a', "{ss}"); if (ret < 0) { return ret; } size_t actions_len = 0, actions_cap = 0; char **actions = NULL; while (true) { ret = sd_bus_message_enter_container(msg, 'e', "ss"); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char *key = NULL, *title = NULL; ret = sd_bus_message_read(msg, "ss", &key, &title); if (ret < 0) { return ret; } // Need space for key, title and NULL terminator if (actions_len + 3 > actions_cap) { actions_cap *= 2; if (actions_cap == 0) { actions_cap = 32; } actions = realloc(actions, actions_cap * sizeof(char *)); } actions[actions_len] = strdup(key); actions[actions_len + 1] = strdup(title); actions_len += 2; 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; } if (actions != NULL) { actions[actions_len] = NULL; } *out = actions; return 0; } static void free_strv(char **strv) { if (strv == NULL) { return; } for (size_t i = 0; strv[i] != NULL; i++) { free(strv[i]); } free(strv); } static bool is_empty_str(const char *str) { return str == NULL || str[0] == '\0'; } static int print_notification(sd_bus_message *reply) { uint32_t id = 0; const char *summary = NULL, *app_name = NULL, *category = NULL, *desktop_entry = NULL; uint8_t urgency = -1; char **actions = NULL; while (true) { int ret = sd_bus_message_enter_container(reply, 'e', "sv"); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char *key = NULL; ret = sd_bus_message_read(reply, "s", &key); if (ret < 0) { return ret; } if (strcmp(key, "id") == 0) { ret = sd_bus_message_read(reply, "v", "u", &id); } else if (strcmp(key, "actions") == 0) { ret = read_actions(reply, &actions); } else if (strcmp(key, "summary") == 0) { ret = sd_bus_message_read(reply, "v", "s", &summary); } else if (strcmp(key, "app-name") == 0) { ret = sd_bus_message_read(reply, "v", "s", &app_name); } else if (strcmp(key, "category") == 0) { ret = sd_bus_message_read(reply, "v", "s", &category); } else if (strcmp(key, "desktop-entry") == 0) { ret = sd_bus_message_read(reply, "v", "s", &desktop_entry); } else if (strcmp(key, "urgency") == 0) { ret = sd_bus_message_read(reply, "v", "y", &urgency); } else { ret = sd_bus_message_skip(reply, "v"); } if (ret < 0) { return ret; } ret = sd_bus_message_exit_container(reply); if (ret < 0) { return ret; } } printf("Notification %" PRIu32 ":", id); if (!is_empty_str(summary)) { printf(" %s", summary); } printf("\n"); if (!is_empty_str(app_name)) { printf(" App name: %s\n", app_name); } if (!is_empty_str(category)) { printf(" Category: %s\n", category); } if (!is_empty_str(desktop_entry)) { printf(" Desktop entry: %s\n", desktop_entry); } const char *urgency_desc = NULL; switch (urgency) { case 0: urgency_desc = "low"; break; case 1: urgency_desc = "normal"; break; case 2: urgency_desc = "critical"; break; } if (urgency_desc != NULL) { printf(" Urgency: %s\n", urgency_desc); } if (actions != NULL) { printf(" Actions:\n"); for (size_t i = 0; actions[i] != NULL; i += 2) { const char *key = actions[i], *title = actions[i + 1]; printf(" %s: %s\n", key, title); } } free_strv(actions); return 0; } static int print_notification_list(sd_bus_message *reply) { int ret = sd_bus_message_enter_container(reply, 'a', "a{sv}"); if (ret < 0) { return ret; } while (true) { ret = sd_bus_message_enter_container(reply, 'a', "{sv}"); if (ret < 0) { return ret; } else if (ret == 0) { break; } ret = print_notification(reply); if (ret < 0) { return ret; } ret = sd_bus_message_exit_container(reply); if (ret < 0) { return ret; } } return sd_bus_message_exit_container(reply); } static int run_history(sd_bus *bus, int argc, char *argv[]) { sd_bus_message *reply = NULL; int ret = call_method(bus, "ListHistory", &reply, ""); if (ret < 0) { return ret; } ret = print_notification_list(reply); sd_bus_message_unref(reply); return ret; } static int run_list(sd_bus *bus, int argc, char *argv[]) { sd_bus_message *reply = NULL; int ret = call_method(bus, "ListNotifications", &reply, ""); if (ret < 0) { return ret; } ret = print_notification_list(reply); sd_bus_message_unref(reply); return ret; } static int exec_menu(char *argv[], FILE **in, FILE **out, pid_t *pid_ptr) { int in_pipe[2], out_pipe[2]; if (pipe(in_pipe) != 0 || pipe(out_pipe) != 0) { perror("pipe() failed"); return -errno; } pid_t pid = fork(); if (pid < 0) { perror("fork() failed"); return -errno; } else if (pid == 0) { if (dup2(in_pipe[0], STDIN_FILENO) < 0 || dup2(out_pipe[1], STDOUT_FILENO) < 0) { perror("dup2() failed"); _exit(1); } close(in_pipe[0]); close(in_pipe[1]); close(out_pipe[0]); close(out_pipe[1]); execvp(argv[0], argv); perror("execvp() failed"); _exit(1); } close(in_pipe[0]); close(out_pipe[1]); *in = fdopen(in_pipe[1], "w"); *out = fdopen(out_pipe[0], "r"); *pid_ptr = pid; return 0; } static int find_actions(sd_bus_message *reply, uint32_t select_id, uint32_t *id_out, char ***actions_out) { int ret = sd_bus_message_enter_container(reply, 'a', "a{sv}"); if (ret < 0) { return ret; } bool found = false; uint32_t id = 0; char **actions = NULL; while (true) { ret = sd_bus_message_enter_container(reply, 'a', "{sv}"); if (ret < 0) { return ret; } else if (ret == 0) { break; } while (true) { ret = sd_bus_message_enter_container(reply, 'e', "sv"); if (ret < 0) { return ret; } else if (ret == 0) { break; } const char *key = NULL; ret = sd_bus_message_read(reply, "s", &key); if (ret < 0) { return ret; } if (strcmp(key, "id") == 0) { ret = sd_bus_message_read(reply, "v", "u", &id); if (ret < 0) { return ret; } } else if (strcmp(key, "actions") == 0) { ret = read_actions(reply, &actions); if (ret < 0) { return ret; } } else { ret = sd_bus_message_skip(reply, "v"); if (ret < 0) { return ret; } } ret = sd_bus_message_exit_container(reply); if (ret < 0) { return ret; } } if (select_id == 0 || id == select_id) { found = true; break; } free_strv(actions); actions = NULL; id = 0; ret = sd_bus_message_exit_container(reply); if (ret < 0) { return ret; } } ret = sd_bus_message_exit_container(reply); if (ret < 0) { return ret; } if (!found) { fprintf(stderr, "Notification not found\n"); return -ENOENT; } *id_out = id; *actions_out = actions; return 0; } static int run_menu(sd_bus *bus, int argc, char *argv[]) { uint32_t select_id = 0; while (true) { int opt = getopt(argc, argv, "n:"); if (opt == -1) { break; } switch (opt) { case 'n':; int ret = parse_uint32(&select_id, optarg); if (ret < 0) { log_neg_errno(ret, "invalid notification ID"); return 1; } break; default: return -EINVAL; } } if (optind >= argc) { fprintf(stderr, "Missing menu command\n"); return -EINVAL; } char **menu_argv = &argv[optind]; sd_bus_message *reply = NULL; int ret = call_method(bus, "ListNotifications", &reply, ""); if (ret < 0) { return ret; } uint32_t id = 0; char **actions = NULL; ret = find_actions(reply, select_id, &id, &actions); sd_bus_message_unref(reply); if (ret < 0) { return ret; } else if (actions == NULL) { fprintf(stderr, "Notification has no actions\n"); return -ENOENT; } pid_t menu_pid = 0; FILE *in = NULL, *out = NULL; ret = exec_menu(menu_argv, &in, &out, &menu_pid); if (ret < 0) { return ret; } for (size_t i = 0; actions[i] != NULL; i += 2) { const char *title = actions[i + 1]; fprintf(in, "%s\n", title); } fclose(in); char *selected_title = NULL; size_t size = 0; errno = 0; ssize_t n_read = getline(&selected_title, &size, out); if (n_read < 0) { if (feof(out)) { fprintf(stderr, "No action selected\n"); return -ECANCELED; } else { perror("getline() failed"); return -errno; } } fclose(out); if (n_read > 0 && selected_title[n_read - 1] == '\n') { selected_title[n_read - 1] = '\0'; } int stat = 0; if (waitpid(menu_pid, &stat, 0) < 0) { perror("waitpid() failed"); return -errno; } else if (stat != 0) { if (WIFEXITED(stat)) { fprintf(stderr, "Menu failed with exit code %d\n", WEXITSTATUS(stat)); } else if (WIFSIGNALED(stat)) { fprintf(stderr, "Menu failed with signal %d\n", WTERMSIG(stat)); } else { abort(); // unreachable } return -ECANCELED; } char *selected_key = NULL; for (size_t i = 0; actions[i] != NULL; i += 2) { const char *key = actions[i], *title = actions[i + 1]; if (strcmp(title, selected_title) == 0) { selected_key = strdup(key); break; } } if (selected_title != NULL && selected_key == NULL) { fprintf(stderr, "Action not found\n"); return -ENOENT; } free(selected_title); free_strv(actions); ret = call_method(bus, "InvokeAction", NULL, "us", id, selected_key); free(selected_key); return ret; } static int find_mode(char **modes, int modes_len, const char *mode) { for (int i = 0; i < modes_len; i++) { if (strcmp(modes[i], mode) == 0) { return i; } } return -1; } static char **add_mode(char **modes, int *modes_len, const char *mode) { modes = realloc(modes, (*modes_len + 2) * sizeof(modes[0])); modes[*modes_len] = strdup(mode); modes[*modes_len + 1] = NULL; (*modes_len)++; return modes; } static void remove_mode(char **modes, int *modes_len, int i) { free(modes[i]); modes[i] = modes[*modes_len - 1]; modes[*modes_len - 1] = NULL; (*modes_len)--; } static int run_mode(sd_bus *bus, int argc, char *argv[]) { sd_bus_message *reply = NULL; int ret = call_method(bus, "ListModes", &reply, ""); if (ret < 0) { return ret; } char **modes = NULL; ret = sd_bus_message_read_strv(reply, &modes); if (ret < 0) { log_neg_errno(ret, "sd_bus_message_read_strv() failed"); return ret; } int modes_len = 0; while (modes != NULL && modes[modes_len] != NULL) { modes_len++; } bool add_remove_toggle_flag = false, set_flag = false; while (true) { int opt = getopt(argc, argv, "a:r:t:s"); if (opt == -1) { break; } int i; switch (opt) { case 'a': add_remove_toggle_flag = true; modes = add_mode(modes, &modes_len, optarg); break; case 'r': add_remove_toggle_flag = true; i = find_mode(modes, modes_len, optarg); if (i >= 0) { remove_mode(modes, &modes_len, i); } break; case 't': add_remove_toggle_flag = true; i = find_mode(modes, modes_len, optarg); if (i >= 0) { remove_mode(modes, &modes_len, i); } else { modes = add_mode(modes, &modes_len, optarg); } break; case 's': set_flag = true; break; default: return -EINVAL; } } if (add_remove_toggle_flag && set_flag) { fprintf(stderr, "-a/-r/-t and -s cannot be used together\n"); return -EINVAL; } if (set_flag) { for (int i = 0; i < modes_len; i++) { free(modes[i]); } modes_len = argc - optind; modes = realloc(modes, (modes_len + 1) * sizeof(modes[0])); for (int i = 0; i < modes_len; i++) { modes[i] = strdup(argv[optind + i]); } modes[modes_len] = NULL; } else if (optind < argc) { fprintf(stderr, "positional arguments can only be used with -s\n"); return -EINVAL; } if (add_remove_toggle_flag || set_flag) { sd_bus_message *m = NULL; ret = new_method_call(bus, &m, "SetModes"); if (ret < 0) { return ret; } ret = sd_bus_message_append_strv(m, modes); if (ret < 0) { log_neg_errno(ret, "sd_bus_message_append_strv() failed"); return ret; } ret = call(bus, m, NULL); sd_bus_message_unref(m); if (ret < 0) { return ret; } } for (int i = 0; i < modes_len; i++) { printf("%s\n", modes[i]); free(modes[i]); } free(modes); sd_bus_message_unref(reply); return 0; } static const char usage[] = "Usage: makoctl [options...]\n" "\n" "Commands:\n" " dismiss [-n id] Dismiss the notification with the\n" " given id, or the last notification\n" " if none is given\n" " [-a|--all] Dismiss all notifications\n" " [-g|--group] Dismiss all the notifications\n" " [-h|--no-history] Dismiss w/o adding to history\n" " in the last notification's group\n" " restore Restore the most recently expired\n" " notification from the history buffer\n" " invoke [-n id] [action] Invoke an action on the notification\n" " with the given id, or the last\n" " notification if none is given\n" " menu [-n id] [arg ...] Use [args ...] to select one\n" " action to be invoked on the notification\n" " with the given id, or the last\n" " notification if none is given\n" " list List notifications\n" " history List history\n" " reload Reload the configuration file\n" " mode List modes\n" " mode [-a mode]... [-r mode]... Add/remove modes\n" " mode [-t mode]... Toggle modes (add if not present, remove if present)\n" " mode -s mode... Set modes\n" " help Show this help\n"; int main(int argc, char *argv[]) { if (argc <= 1) { fprintf(stderr, "%s", usage); return 1; } const char *cmd = argv[1]; int cmd_argc = argc - 1; char **cmd_argv = &argv[1]; if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0 || strcmp(cmd, "-h") == 0) { fprintf(stderr, "%s", usage); return 0; } sd_bus *bus = NULL; int ret = sd_bus_open_user(&bus); if (ret < 0) { log_neg_errno(ret, "sd_bus_open_user() failed"); return 1; } if (strcmp(cmd, "dismiss") == 0) { ret = run_dismiss(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "invoke") == 0) { ret = run_invoke(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "history") == 0) { ret = run_history(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "list") == 0) { ret = run_list(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "menu") == 0) { ret = run_menu(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "mode") == 0) { ret = run_mode(bus, cmd_argc, cmd_argv); } else if (strcmp(cmd, "reload") == 0) { ret = call_method(bus, "Reload", NULL, ""); } else if (strcmp(cmd, "restore") == 0) { ret = call_method(bus, "RestoreNotification", NULL, ""); } else { fprintf(stderr, "Unknown command: %s\n", cmd); return 1; } sd_bus_unref(bus); return ret >= 0 ? 0 : 1; } mako-notifier-1.10.0/meson.build000066400000000000000000000050271476555712500165240ustar00rootroot00000000000000project( 'mako', 'c', version: '1.10.0', license: 'MIT', meson_version: '>=0.60.0', default_options: [ 'c_std=c11', 'warning_level=2', 'werror=true', ], ) add_project_arguments([ '-D_POSIX_C_SOURCE=200809L', '-Wundef', '-Wno-unused-parameter', '-Wno-missing-braces', ], language: 'c') 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.32') wayland_cursor = dependency('wayland-cursor') epoll = dependency('', required: false) if (not cc.has_function('timerfd_create', prefix: '#include ') or not cc.has_function('signalfd', prefix: '#include ')) epoll = dependency('epoll-shim') endif if get_option('sd-bus-provider') == 'auto' assert(get_option('auto_features').auto(), 'sd-bus-provider must not be set to auto since auto_features != auto') sdbus = dependency('libsystemd', 'libelogind', 'basu') else sdbus = dependency(get_option('sd-bus-provider')) endif add_project_arguments('-DHAVE_' + sdbus.name().to_upper(), language: 'c') gdk_pixbuf = dependency('gdk-pixbuf-2.0', required: get_option('icons')) if gdk_pixbuf.found() add_project_arguments('-DHAVE_ICONS', language: 'c') endif subdir('contrib/completions') subdir('protocol') src_files = [ 'config.c', 'event-loop.c', 'dbus/dbus.c', 'dbus/mako.c', 'dbus/xdg.c', 'main.c', 'mode.c', 'notification.c', 'pool-buffer.c', 'render.c', 'wayland.c', 'criteria.c', 'types.c', 'surface.c', 'icon.c', 'string-util.c', ] if gdk_pixbuf.found() src_files += 'cairo-pixbuf.c' endif executable( 'mako', files(src_files) + protocols_src, dependencies: [ cairo, epoll, gdk_pixbuf, sdbus, pango, pangocairo, glib, gobject, realtime, wayland_client, wayland_cursor, ], include_directories: [mako_inc], install: true, ) executable( 'makoctl', ['makoctl.c'], dependencies: [sdbus], install: true, ) conf_data = configuration_data() conf_data.set('bindir', get_option('prefix') / get_option('bindir')) configure_file( configuration: conf_data, input: 'fr.emersion.mako.service.in', output: '@BASENAME@', install_dir: get_option('datadir') + '/dbus-1/services', ) subdir('doc') summary({ 'sd-bus provider': sdbus.name(), 'Icons': gdk_pixbuf.found(), 'Man pages': scdoc.found(), }, bool_yn: true) mako-notifier-1.10.0/meson_options.txt000066400000000000000000000011741476555712500200160ustar00rootroot00000000000000option('sd-bus-provider', type: 'combo', choices: ['auto', 'libsystemd', 'libelogind', 'basu'], value: 'auto', description: 'Provider of the sd-bus library') option('icons', type: 'feature', value: 'auto', description: 'Enable icon support') option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') option('fish-completions', type: 'boolean', value: false, description: 'Install fish completions') option('zsh-completions', type: 'boolean', value: false, description: 'Install zsh completions') option('bash-completions', type: 'boolean', value: false, description: 'Install bash completions') mako-notifier-1.10.0/mode.c000066400000000000000000000016161476555712500154520ustar00rootroot00000000000000#include #include #include #include "mako.h" #include "mode.h" #include "dbus.h" bool has_mode(struct mako_state *state, const char *mode) { const char **mode_ptr; wl_array_for_each(mode_ptr, &state->current_modes) { if (strcmp(*mode_ptr, mode) == 0) { return true; } } return false; } void set_modes(struct mako_state *state, const char **modes, size_t modes_len) { char **mode_ptr; wl_array_for_each(mode_ptr, &state->current_modes) { free(*mode_ptr); } state->current_modes.size = 0; for (size_t i = 0; i < modes_len; i++) { // Drop duplicate entries bool dup = false; for (size_t j = 0; j < i; j++) { if (strcmp(modes[i], modes[j]) == 0) { dup = true; break; } } if (dup) { continue; } char **dst = wl_array_add(&state->current_modes, sizeof(char *)); *dst = strdup(modes[i]); } emit_modes_changed(state); } mako-notifier-1.10.0/notification.c000066400000000000000000000365541476555712500172250ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #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" #include "wayland.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); free(notif->tag); if (notif->image_data != NULL) { free(notif->image_data->data); free(notif->image_data); } notif->app_name = strdup(""); notif->app_icon = strdup(""); notif->summary = strdup(""); notif->body = strdup(""); notif->category = strdup(""); notif->desktop_entry = strdup(""); notif->tag = strdup(""); 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); wl_list_init(¬if->link); 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); free(notif->app_name); free(notif->app_icon); free(notif->summary); free(notif->body); free(notif->category); free(notif->desktop_entry); free(notif->tag); finish_style(¬if->style); free(notif); } void close_notification(struct mako_notification *notif, enum mako_notification_close_reason reason, bool add_to_history) { 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); destroy_criteria(notif_criteria); } if (!notif->style.history || notif->state->config.max_history <= 0) { destroy_notification(notif); return; } destroy_timer(notif->timer); notif->timer = NULL; if (add_to_history) { wl_list_insert(¬if->state->history, ¬if->link); while (wl_list_length(¬if->state->history) > notif->state->config.max_history) { struct mako_notification *n = wl_container_of(notif->state->history.prev, n, link); destroy_notification(n); } } else { 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; } struct mako_notification *get_tagged_notification(struct mako_state *state, const char *tag, const char *app_name) { struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (notif->tag && strlen(notif->tag) != 0 && strcmp(notif->tag, tag) == 0 && strcmp(notif->app_name, app_name) == 0) { return notif; } } return NULL; } void close_group_notifications(struct mako_notification *top_notif, enum mako_notification_close_reason reason, bool add_to_history) { struct mako_state *state = top_notif->state; if (top_notif->style.group_criteria_spec.none) { // No grouping, just close the notification close_notification(top_notif, reason, add_to_history); return; } struct mako_criteria *notif_criteria = create_criteria_from_notification( top_notif, &top_notif->style.group_criteria_spec); struct mako_notification *notif, *tmp; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { if (match_criteria(notif_criteria, notif)) { close_notification(notif, reason, add_to_history); } } destroy_criteria(notif_criteria); } void close_all_notifications(struct mako_state *state, enum mako_notification_close_reason reason, bool add_to_history) { struct mako_notification *notif, *tmp; wl_list_for_each_safe(notif, tmp, &state->notifications, link) { close_notification(notif, reason, add_to_history); } } 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 'i': return mako_asprintf("%d", notif->id); 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 const struct mako_binding *get_button_binding(struct mako_style *style, uint32_t button) { switch (button) { case BTN_LEFT: return &style->button_bindings.left; case BTN_RIGHT: return &style->button_bindings.right; case BTN_MIDDLE: return &style->button_bindings.middle; } return NULL; } static void try_invoke_action(struct mako_notification *notif, const char *target_action, const struct mako_binding_context *ctx) { struct mako_action *action; wl_list_for_each(action, ¬if->actions, link) { if (strcmp(action->key, target_action) == 0) { char *activation_token = NULL; if (ctx != NULL) { activation_token = create_xdg_activation_token( ctx->surface, ctx->seat, ctx->serial); } notify_action_invoked(action, activation_token); free(activation_token); break; } } close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, true); } void notification_execute_binding(struct mako_notification *notif, const struct mako_binding *binding, const struct mako_binding_context *ctx) { switch (binding->action) { case MAKO_BINDING_NONE: break; case MAKO_BINDING_DISMISS: close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, true); break; case MAKO_BINDING_DISMISS_NO_HISTORY: close_notification(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, false); break; case MAKO_BINDING_DISMISS_GROUP: close_group_notifications(notif, MAKO_NOTIFICATION_CLOSE_DISMISSED, true); break; case MAKO_BINDING_DISMISS_ALL: close_all_notifications(notif->state, MAKO_NOTIFICATION_CLOSE_DISMISSED, true); break; case MAKO_BINDING_INVOKE_ACTION: assert(binding->action_name != NULL); try_invoke_action(notif, binding->action_name, ctx); break; case MAKO_BINDING_EXEC: assert(binding->command != NULL); pid_t pid = fork(); if (pid < 0) { perror("fork failed"); break; } else if (pid == 0) { // Double-fork to avoid SIGCHLD issues pid = fork(); if (pid < 0) { perror("fork failed"); _exit(1); } else if (pid == 0) { // We pass variables using additional sh arguments. To convert // back the arguments to variables, insert a short script // preamble before the user's command. const char setup_vars[] = "id=\"$1\"\n"; char *cmd = mako_asprintf("%s%s", setup_vars, binding->command); char id_str[32]; snprintf(id_str, sizeof(id_str), "%" PRIu32, notif->id); char *const argv[] = { "sh", "-c", cmd, "sh", id_str, NULL }; execvp("sh", argv); perror("exec failed"); _exit(1); } _exit(0); } if (waitpid(pid, NULL, 0) < 0) { perror("waitpid failed"); } break; } } void notification_handle_button(struct mako_notification *notif, uint32_t button, enum wl_pointer_button_state state, const struct mako_binding_context *ctx) { if (state != WL_POINTER_BUTTON_STATE_PRESSED) { return; } const struct mako_binding *binding = get_button_binding(¬if->style, button); if (binding != NULL) { notification_execute_binding(notif, binding, ctx); } } void notification_handle_touch(struct mako_notification *notif, const struct mako_binding_context *ctx) { notification_execute_binding(notif, ¬if->style.touch_binding, ctx); } /* * 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 specified. * (-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_CRITICAL && 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; } // Place all of the matches back into the list where the first one was // originally. wl_list_insert_list(location, &matches); // We don't actually re-apply criteria here, that will happen just before // we render each notification anyway. return count; } mako-notifier-1.10.0/pool-buffer.c000066400000000000000000000062231476555712500167450ustar00rootroot00000000000000#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.10.0/protocol/000077500000000000000000000000001476555712500162175ustar00rootroot00000000000000mako-notifier-1.10.0/protocol/meson.build000066400000000000000000000020521476555712500203600ustar00rootroot00000000000000wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir') wayland_scanner = dependency('wayland-scanner', version: '>=1.14.91', native: true) wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner') wayland_scanner_prog = find_program(wayland_scanner_path, native: true) wayland_scanner_code = generator( wayland_scanner_prog, output: '@BASENAME@-protocol.c', arguments: ['private-code', '@INPUT@', '@OUTPUT@'], ) wayland_scanner_client = generator( wayland_scanner_prog, output: '@BASENAME@-client-protocol.h', arguments: ['client-header', '@INPUT@', '@OUTPUT@'], ) protocols = [ wl_protocol_dir / 'stable/xdg-shell/xdg-shell.xml', wl_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml', wl_protocol_dir / 'staging/xdg-activation/xdg-activation-v1.xml', wl_protocol_dir / 'unstable/tablet/tablet-unstable-v2.xml', 'wlr-layer-shell-unstable-v1.xml', ] protocols_src = [] foreach p : protocols protocols_src += wayland_scanner_code.process(p) protocols_src += wayland_scanner_client.process(p) endforeach mako-notifier-1.10.0/protocol/wlr-layer-shell-unstable-v1.xml000066400000000000000000000320701476555712500241250ustar00rootroot00000000000000 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.10.0/render.c000066400000000000000000000373411476555712500160110ustar00rootroot00000000000000#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" #define M_PI 3.14159265358979323846 // 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_top_left, int radius_top_right, int radius_bottom_right, int radius_bottom_left) { if (width == 0 || height == 0) { return; } x *= scale; y *= scale; width *= scale; height *= scale; radius_top_left *= scale; radius_top_right *= scale; radius_bottom_right *= scale; radius_bottom_left *= scale; double degrees = M_PI / 180.0; cairo_new_sub_path(cairo); cairo_arc(cairo, x + radius_top_left, y + radius_top_left, radius_top_left, 180 * degrees, 270 * degrees); cairo_arc(cairo, x + width - radius_top_right, y + radius_top_right, radius_top_right, -90 * degrees, 0 * degrees); cairo_arc(cairo, x + width - radius_bottom_right, y + height - radius_bottom_right, radius_bottom_right, 0 * degrees, 90 * degrees); cairo_arc(cairo, x + radius_bottom_left, y + height - radius_bottom_left, radius_bottom_left, 90 * degrees, 180 * 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; } abort(); } static void set_font_options(cairo_t *cairo, struct mako_surface *surface) { if (surface->surface_output == NULL) { return; } cairo_font_options_t *fo = cairo_font_options_create(); if (surface->surface_output->subpixel == WL_OUTPUT_SUBPIXEL_NONE) { cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_GRAY); } else { cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_SUBPIXEL); cairo_font_options_set_subpixel_order(fo, get_cairo_subpixel_order(surface->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_surface *surface, 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_top_left = style->border_radius.top; int radius_top_right = style->border_radius.right; int radius_bottom_right = style->border_radius.bottom; int radius_bottom_left = style->border_radius.left; int icon_radius = style->icon_border_radius; bool icon_vertical = style->icon_location == MAKO_ICON_LOCATION_TOP || style->icon_location == MAKO_ICON_LOCATION_BOTTOM; // If the compositor has forced us to shrink down, do so. int notif_width = (style->width <= surface->width) ? style->width : surface->width; // offset_x is for the entire draw operation inside the surface int offset_x; if (surface->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT) { offset_x = surface->width - notif_width - style->margin.right; } else if (surface->anchor & ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT) { offset_x = style->margin.left; } else { // CENTER has nothing to & with, so it's the else case offset_x = (surface->width - notif_width) / 2; } // text_x is the offset of the text inside our draw operation double text_x = style->padding.left; if (icon != NULL && style->icon_location == MAKO_ICON_LOCATION_LEFT) { text_x = icon->width + 2*style->padding.left; } // text_y is the offset of the text inside our draw operation double text_y = style->padding.top; if (icon != NULL && style->icon_location == MAKO_ICON_LOCATION_TOP) { text_y = icon->height + 2*style->padding.top; } double text_layout_width = notif_width - border_size - padding_width; if (icon && ! icon_vertical) { text_layout_width -= icon->width; text_layout_width -= style->icon_location == MAKO_ICON_LOCATION_LEFT ? (style->padding.left * 2) : (style->padding.right * 2); } double text_layout_height = style->height - border_size - padding_height; if (icon && icon_vertical) { text_layout_height -= icon->height; text_layout_height -= style->icon_location == MAKO_ICON_LOCATION_TOP ? (style->padding.top * 2) : (style->padding.bottom * 2); } set_font_options(cairo, surface); PangoLayout *layout = pango_cairo_create_layout(cairo); set_layout_size(layout, text_layout_width, text_layout_height, scale); pango_layout_set_alignment(layout, style->text_alignment); 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; int buffer_text_width = 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, &buffer_text_width, &buffer_text_height); } int text_height = buffer_text_height / scale; int text_width = buffer_text_width / scale; if (text_height > text_layout_height) { text_height = text_layout_height; } int notif_height = text_height + border_size + padding_height; if (icon && icon_vertical) { notif_height += icon->height; notif_height += style->icon_location == MAKO_ICON_LOCATION_TOP ? style->padding.top : style->padding.bottom; } if (icon != NULL && ! icon_vertical && icon->height > text_height) { notif_height = icon->height + border_size + padding_height; } if (notif_height < radius_top_left + radius_bottom_left) { notif_height = radius_top_left + radius_bottom_left + border_size; } if (notif_height < radius_top_right + radius_bottom_right) { notif_height = radius_top_right + radius_bottom_right + border_size; } int notif_background_width = notif_width - style->border_size; // Define the shape of the notification. The stroke is drawn centered on // the edge of the fill, so we need to inset the shape by half the // border_size. set_rounded_rectangle(cairo, offset_x + style->border_size / 2.0, offset_y + style->border_size / 2.0, notif_background_width, notif_height - style->border_size, scale, radius_top_left, radius_top_right, radius_bottom_right, radius_bottom_left); // Render background, keeping the path. set_source_u32(cairo, style->colors.background); cairo_fill_preserve(cairo); // Keep a copy of the path. We need it later to draw the border on top, but // we have to create a new one for progress in the meantime. cairo_path_t *border_path = cairo_copy_path(cairo); // Render progress. We need to render this as a normal rectangle, but clip // it to the rounded rectangle we drew for the background. We also inset it // a bit further so that 0 and 100 percent are aligned to the inside edge // of the border and we can actually see the whole range. int progress_width = (notif_background_width - style->border_size) * progress / 100; if (progress_width < 0) { progress_width = 0; } else if (progress_width > notif_background_width) { progress_width = notif_background_width - style->border_size; } cairo_save(cairo); cairo_clip(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 - style->border_size, scale, 0, 0, 0, 0); cairo_fill(cairo); cairo_restore(cairo); // Render border, using the SOURCE operator to clip away the background // and progress beneath. This is the only way to make the background appear // to line up with the inside of a rounded border, while not revealing any // of the background when using a translucent border color. cairo_save(cairo); cairo_append_path(cairo, border_path); set_source_u32(cairo, style->colors.border); cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); cairo_set_line_width(cairo, style->border_size * scale); cairo_stroke(cairo); cairo_restore(cairo); cairo_path_destroy(border_path); if (icon != NULL) { // Render icon double xpos = -1; double ypos = -1; double ypos_center = offset_y + style->border_size + (notif_height - icon->height - border_size) / 2; double xpos_center = offset_x + style->border_size + (notif_width - icon->width - border_size) / 2; switch (style->icon_location) { case MAKO_ICON_LOCATION_LEFT: xpos = offset_x + style->border_size + style->padding.left; ypos = ypos_center; break; case MAKO_ICON_LOCATION_RIGHT: xpos = offset_x + notif_width - style->border_size - style->padding.right - icon->width; ypos = ypos_center; break; case MAKO_ICON_LOCATION_TOP: xpos = xpos_center; ypos = offset_y + style->border_size + style->padding.top; break; case MAKO_ICON_LOCATION_BOTTOM: xpos = xpos_center; ypos = offset_y + notif_height - style->border_size - style->padding.bottom - icon->height; break; } cairo_save(cairo); set_rounded_rectangle(cairo, xpos, ypos, icon->width, icon->height, scale, icon_radius, icon_radius, icon_radius, icon_radius); cairo_clip(cairo); draw_icon(cairo, icon, xpos, ypos, scale); cairo_restore(cairo); } if (icon_vertical) { text_x = (notif_width - text_width - border_size) / 2; } else { text_y = (notif_height - text_height - border_size) / 2; } // Render text set_source_u32(cairo, style->colors.text); move_to(cairo, offset_x + style->border_size + text_x, offset_y + style->border_size + text_y, 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; } void render(struct mako_surface *surface, struct pool_buffer *buffer, int scale, int *rendered_width, int *rendered_height) { struct mako_state *state = surface->state; cairo_t *cairo = buffer->cairo; *rendered_width = *rendered_height = 0; if (wl_list_empty(&state->notifications)) { return; } // 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 visible_count = 0; size_t hidden_count = 0; int total_height = 0; int max_width = 0; int pending_bottom_margin = 0; struct mako_notification *notif; size_t total_notifications = 0; wl_list_for_each(notif, &state->notifications, link) { if (notif->surface != surface) { continue; } ++total_notifications; // Immediately before rendering we need to re-match all of the criteria // so that matches against the anchor and output work even if the // output was automatically assigned by the compositor. 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"); break; } 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"); break; } // 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; if (style->max_visible >= 0 && visible_count >= (size_t)style->max_visible) { ++hidden_count; continue; } 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, surface, style, text, icon, total_height, scale, ¬if->hotspot, notif->progress); free(text); int notif_width = style->width + style->margin.left + style->margin.right; total_height += notif_height; if (max_width < notif_width) { max_width = notif_width; } 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 (hidden_count > 0) { struct mako_notification *hidden_notif = create_notification(state); hidden_notif->surface = surface; hidden_notif->hidden = true; apply_each_criteria(&state->config.criteria, hidden_notif); struct mako_style *style = &hidden_notif->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 = hidden_count, .count = total_notifications, }; 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; } format_text(style->format, text, format_hidden_text, &data); int hidden_height = render_notification( cairo, state, surface, style, text, NULL, total_height, scale, NULL, 0); free(text); total_height += hidden_height; pending_bottom_margin = style->margin.bottom; destroy_notification(hidden_notif); } *rendered_width = max_width; *rendered_height = total_height; } mako-notifier-1.10.0/string-util.c000066400000000000000000000006311476555712500170030ustar00rootroot00000000000000#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.10.0/surface.c000066400000000000000000000020051476555712500161470ustar00rootroot00000000000000#include #include #include "mako.h" #include "surface.h" void destroy_surface(struct mako_surface *surface) { if (surface->layer_surface != NULL) { zwlr_layer_surface_v1_destroy(surface->layer_surface); } if (surface->surface != NULL) { wl_surface_destroy(surface->surface); } if (surface->frame_callback != NULL) { wl_callback_destroy(surface->frame_callback); } finish_buffer(&surface->buffers[0]); finish_buffer(&surface->buffers[1]); /* Clean up memory resources */ free(surface->configured_output); wl_list_remove(&surface->link); free(surface); } struct mako_surface *create_surface(struct mako_state *state, const char *output, enum zwlr_layer_shell_v1_layer layer, uint32_t anchor) { struct mako_surface *surface = calloc(1, sizeof(*surface)); if (!surface) { return NULL; } surface->configured_output = strdup(output); surface->layer = layer; surface->anchor = anchor; surface->state = state; wl_list_insert(&state->surfaces, &surface->link); return surface; } mako-notifier-1.10.0/types.c000066400000000000000000000211021476555712500156620ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include "enum.h" #include "types.h" const char VALID_FORMAT_SPECIFIERS[] = "%asbhtgi"; 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; int parsed; parsed = (int)strtol(string, &end, 10); if (errno == 0 && end[0] == '\0') { *out = parsed; return true; } else { return false; } } bool parse_int_ge(const char *string, int *out, int min) { int parsed; if (parse_int(string, &parsed) && parsed >= min) { *out = parsed; return true; } else { return false; } } 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) { free(components); 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, "critical") == 0) { *out = MAKO_NOTIFICATION_URGENCY_CRITICAL; return true; } else if (strcasecmp(string, "high") == 0) { *out = MAKO_NOTIFICATION_URGENCY_CRITICAL; 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, "grouped") == 0) { out->grouped = true; } else if (strcmp(token, "group-index") == 0) { out->group_index = true; } else if (strcmp(token, "anchor") == 0) { out->anchor = true; } else if (strcmp(token, "output") == 0) { out->output = true; } else if (strcmp(token, "none") == 0) { out->none = true; } else { fprintf(stderr, "Unknown criteria field '%s'\n", token); free(components); return false; } token = strtok_r(NULL, ",", &saveptr); } free(components); return true; } // Checks whether any of the fields of the given specification are set. Useful // for checking for some subset of fields without enumerating all known fields // yourself. Often you will want to copy a spec and clear fields you _don't_ // care about to use this. bool mako_criteria_spec_any(const struct mako_criteria_spec *spec) { return spec->app_name || spec->app_icon || spec->actionable || spec->expiring || spec->urgency || spec->category || spec->desktop_entry || spec->summary || spec->summary_pattern || spec->body || spec->body_pattern || spec->none || spec->group_index || spec->grouped || spec->hidden || spec->output || spec->anchor; } 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; } bool parse_anchor(const char *string, uint32_t *out) { if (strcmp(string, "top-right") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } else if (strcmp(string, "top-center") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; } else if (strcmp(string, "top-left") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; } else if (strcmp(string, "bottom-right") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } else if (strcmp(string, "bottom-center") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; } else if (strcmp(string, "bottom-left") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; } else if (strcmp(string, "center-right") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; } else if (strcmp(string, "center-left") == 0) { *out = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT; } else if (strcmp(string, "center") == 0) { *out = 0; } else { fprintf(stderr, "Invalid anchor value '%s'\n", string); return false; } return true; } mako-notifier-1.10.0/wayland.c000066400000000000000000000570761476555712500162000ustar00rootroot00000000000000#include #include #include #include #include #include "criteria.h" #include "mako.h" #include "notification.h" #include "render.h" #include "surface.h" #include "wayland.h" static void noop() { // This space intentionally left blank } 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 void output_handle_name(void *data, struct wl_output *wl_output, const char *name) { struct mako_output *output = data; output->name = strdup(name); } static const struct wl_output_listener output_listener = { .geometry = output_handle_geometry, .mode = noop, .done = noop, .scale = output_handle_scale, .name = output_handle_name, .description = noop, }; static void send_frame(struct mako_surface *surface); 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)); struct mako_surface *surface; bool recreate_surface = false; 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; recreate_surface = wl_list_empty(&state->outputs); wl_list_insert(&state->outputs, &output->link); wl_output_set_user_data(wl_output, output); wl_output_add_listener(wl_output, &output_listener, output); if (recreate_surface) { // We had no outputs, force our surfaces to redraw wl_list_for_each(surface, &output->state->surfaces, link) { set_dirty(surface); } } } static void destroy_output(struct mako_output *output) { struct mako_surface *surface; wl_list_for_each(surface, &output->state->surfaces, link) { if (surface->surface_output == output) { surface->surface_output = NULL; } if (surface->layer_surface_output == output) { surface->layer_surface_output = NULL; } } wl_list_remove(&output->link); wl_output_destroy(output->wl_output); free(output->name); free(output); } static struct mako_surface *get_surface(struct mako_state *state, struct wl_surface *wl_surface) { struct mako_surface *surface; wl_list_for_each(surface, &state->surfaces, link) { if (surface->surface == wl_surface) { return surface; } } return NULL; } 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; if (id >= MAX_TOUCHPOINTS) { return; } seat->touch.pts[id].x = wl_fixed_to_int(surface_x); seat->touch.pts[id].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 *wl_surface, int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct mako_seat *seat = data; if (id >= MAX_TOUCHPOINTS) { return; } seat->touch.pts[id].x = wl_fixed_to_int(surface_x); seat->touch.pts[id].y = wl_fixed_to_int(surface_y); seat->touch.pts[id].surface = get_surface(seat->state, wl_surface); } 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; if (id >= MAX_TOUCHPOINTS) { return; } const struct mako_binding_context ctx = { .surface = seat->touch.pts[id].surface, .seat = seat, .serial = serial, }; struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (hotspot_at(¬if->hotspot, seat->touch.pts[id].x, seat->touch.pts[id].y)) { struct mako_surface *surface = notif->surface; notification_handle_touch(notif, &ctx); set_dirty(surface); break; } } seat->touch.pts[id].surface = NULL; } static void load_default_cursor(struct mako_state *state, uint32_t scale) { const char *cursor_name = "left_ptr"; //don't reload the cursor if what we have already can be used if (state->cursor.theme != NULL && state->cursor.scale == scale) { return; } if (state->cursor.theme != NULL) { wl_cursor_theme_destroy(state->cursor.theme); } if (state->cursor.surface == NULL) { state->cursor.surface = wl_compositor_create_surface(state->compositor); } const char *xcursor_theme = getenv("XCURSOR_THEME"); state->cursor.theme = wl_cursor_theme_load(xcursor_theme, state->cursor.size * scale, state->shm); if (state->cursor.theme == NULL) { fprintf(stderr, "couldn't find a cursor theme\n"); return; } struct wl_cursor *cursor = wl_cursor_theme_get_cursor(state->cursor.theme, cursor_name); if (cursor == NULL) { fprintf(stderr, "couldn't find cursor icon \"%s\"\n", cursor_name); wl_cursor_theme_destroy(state->cursor.theme); // Set to NULL so it doesn't get free'd again state->cursor.theme = NULL; return; } state->cursor.scale = scale; state->cursor.image = cursor->images[0]; struct wl_buffer *cursor_buffer = wl_cursor_image_get_buffer(cursor->images[0]); wl_surface_attach(state->cursor.surface, cursor_buffer, 0, 0); wl_surface_set_buffer_scale(state->cursor.surface, scale); wl_surface_commit(state->cursor.surface); } static void pointer_handle_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *wl_surface, wl_fixed_t surface_x, wl_fixed_t surface_y) { struct mako_seat *seat = data; struct mako_state *state = seat->state; seat->pointer.x = wl_fixed_to_int(surface_x); seat->pointer.y = wl_fixed_to_int(surface_y); seat->pointer.surface = get_surface(state, wl_surface); int scale = 1; struct mako_surface *surface; wl_list_for_each(surface, &state->surfaces, link) { if (!surface->surface_output) { continue; } if (surface->surface_output->scale > scale) { scale = surface->surface_output->scale; } } if (state->cursor_shape_manager != NULL) { struct wp_cursor_shape_device_v1 *device = wp_cursor_shape_manager_v1_get_pointer(state->cursor_shape_manager, wl_pointer); wp_cursor_shape_device_v1_set_shape(device, serial, WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_DEFAULT); wp_cursor_shape_device_v1_destroy(device); } else { load_default_cursor(state, scale); if (state->cursor.theme != NULL) { wl_pointer_set_cursor(wl_pointer, serial, state->cursor.surface, state->cursor.image->hotspot_x / state->cursor.scale, state->cursor.image->hotspot_y / state->cursor.scale); } } } static void pointer_handle_leave(void *data, struct wl_pointer *wl_pointer, uint32_t serial, struct wl_surface *wl_surface) { struct mako_seat *seat = data; seat->pointer.surface = NULL; } 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; const struct mako_binding_context ctx = { .surface = seat->pointer.surface, .seat = seat, .serial = serial, }; struct mako_notification *notif; wl_list_for_each(notif, &state->notifications, link) { if (hotspot_at(¬if->hotspot, seat->pointer.x, seat->pointer.y)) { struct mako_surface *surface = notif->surface; notification_handle_button(notif, button, button_state, &ctx); set_dirty(surface); break; } } } static const struct wl_pointer_listener pointer_listener = { .enter = pointer_handle_enter, .leave = pointer_handle_leave, .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_surface *msurface = data; // Don't bother keeping a list of outputs, a layer surface can only be on // one output a a time msurface->surface_output = wl_output_get_user_data(wl_output); set_dirty(msurface); } static void surface_handle_leave(void *data, struct wl_surface *surface, struct wl_output *wl_output) { struct mako_surface *msurface = data; msurface->surface_output = NULL; } static const struct wl_surface_listener surface_listener = { .enter = surface_handle_enter, .leave = surface_handle_leave, }; static void schedule_frame_and_commit(struct mako_surface *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_surface *msurface = data; zwlr_layer_surface_v1_ack_configure(surface, serial); if (msurface->configured && msurface->width == (int32_t) width && msurface->height == (int32_t) height) { wl_surface_commit(msurface->surface); return; } msurface->configured = true; msurface->width = width; msurface->height = height; send_frame(msurface); } static void layer_surface_handle_closed(void *data, struct zwlr_layer_surface_v1 *surface) { struct mako_surface *msurface = data; zwlr_layer_surface_v1_destroy(msurface->layer_surface); msurface->layer_surface = NULL; wl_surface_destroy(msurface->surface); msurface->surface = NULL; if (msurface->frame_callback) { wl_callback_destroy(msurface->frame_callback); msurface->frame_callback = NULL; msurface->dirty = true; } if (msurface->configured) { msurface->configured = false; msurface->width = msurface->height = 0; msurface->dirty = true; } if (msurface->dirty) { schedule_frame_and_commit(msurface); } } 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, 4); create_output(state, output, name); } else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { state->xdg_activation = wl_registry_bind(registry, name, &xdg_activation_v1_interface, 1); } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { state->cursor_shape_manager = wl_registry_bind(registry, name, &wp_cursor_shape_manager_v1_interface, 1); } } 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); if (wl_display_roundtrip(state->display) < 0) { fprintf(stderr, "wl_display_roundtrip() failed\n"); return false; } 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; } // Second roundtrip to get output metadata if (wl_display_roundtrip(state->display) < 0) { fprintf(stderr, "wl_display_roundtrip() failed\n"); return false; } // Set up the cursor. It needs a wl_surface with the cursor loaded into it. // If one of these fail, mako will work fine without the cursor being able to change. const char *cursor_size_env = getenv("XCURSOR_SIZE"); int cursor_size = 24; if (cursor_size_env != NULL) { errno = 0; char *end; int temp_size = (int)strtol(cursor_size_env, &end, 10); if (errno == 0 && cursor_size_env[0] != 0 && end[0] == 0 && temp_size > 0) { cursor_size = temp_size; } else { fprintf(stderr, "Error: XCURSOR_SIZE is invalid\n"); } } state->cursor.size = cursor_size; return true; } void finish_wayland(struct mako_state *state) { struct mako_surface *surface, *stmp; wl_list_for_each_safe(surface, stmp, &state->surfaces, link) { destroy_surface(surface); } 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_activation != NULL) { xdg_activation_v1_destroy(state->xdg_activation); } if (state->cursor_shape_manager != NULL) { wp_cursor_shape_manager_v1_destroy(state->cursor_shape_manager); } if (state->cursor.theme != NULL) { wl_cursor_theme_destroy(state->cursor.theme); wl_surface_destroy(state->cursor.surface); } 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_surface *surface) { struct wl_region *region = wl_compositor_create_region(surface->state->compositor); struct mako_notification *notif; wl_list_for_each(notif, &surface->state->notifications, link) { struct mako_hotspot *hotspot = ¬if->hotspot; if (notif->surface == surface) { wl_region_add(region, hotspot->x, hotspot->y, hotspot->width, hotspot->height); } } return region; } static struct mako_output *get_configured_output(struct mako_surface *surface) { const char *output_name = surface->configured_output; if (strcmp(output_name, "") == 0) { return NULL; } struct mako_output *output; wl_list_for_each(output, &surface->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_surface *surface); // Draw and commit a new frame. static void send_frame(struct mako_surface *surface) { struct mako_state *state = surface->state; if (wl_list_empty(&state->outputs)) { surface->dirty = false; return; } int scale = 1; if (surface->surface_output != NULL) { scale = surface->surface_output->scale; } surface->current_buffer = get_next_buffer(state->shm, surface->buffers, surface->width * scale, surface->height * scale); if (surface->current_buffer == NULL) { fprintf(stderr, "no buffer available\n"); return; } struct mako_output *output = get_configured_output(surface); int width = 0, height = 0; render(surface, surface->current_buffer, scale, &width, &height); // There are two cases where we want to tear down the surface: zero // notifications (height = 0) or moving between outputs. if (height == 0 || surface->layer_surface_output != output) { if (surface->layer_surface != NULL) { zwlr_layer_surface_v1_destroy(surface->layer_surface); surface->layer_surface = NULL; } if (surface->surface != NULL) { wl_surface_destroy(surface->surface); surface->surface = NULL; } surface->width = surface->height = 0; surface->surface_output = NULL; surface->configured = false; } // If there are no notifications, there's no point in recreating the // surface right now. if (height == 0) { surface->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 (surface->layer_surface == NULL) { struct wl_output *wl_output = NULL; if (output != NULL) { wl_output = output->wl_output; } surface->layer_surface_output = output; surface->surface = wl_compositor_create_surface(state->compositor); wl_surface_add_listener(surface->surface, &surface_listener, surface); surface->layer_surface = zwlr_layer_shell_v1_get_layer_surface( state->layer_shell, surface->surface, wl_output, surface->layer, "notifications"); zwlr_layer_surface_v1_add_listener(surface->layer_surface, &layer_surface_listener, surface); // 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(surface->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 (surface->height != height || surface->width != width) { struct mako_style *style = &state->config.superstyle; zwlr_layer_surface_v1_set_size(surface->layer_surface, width, height); zwlr_layer_surface_v1_set_anchor(surface->layer_surface, surface->anchor); zwlr_layer_surface_v1_set_margin(surface->layer_surface, style->outer_margin.top, style->outer_margin.right, style->outer_margin.bottom, style->outer_margin.left); wl_surface_commit(surface->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(surface->configured); // Yay we can finally draw something! struct wl_region *input_region = get_input_region(surface); wl_surface_set_input_region(surface->surface, input_region); wl_region_destroy(input_region); wl_surface_set_buffer_scale(surface->surface, scale); wl_surface_damage_buffer(surface->surface, 0, 0, INT32_MAX, INT32_MAX); wl_surface_attach(surface->surface, surface->current_buffer->buffer, 0, 0); surface->current_buffer->busy = true; // Schedule a frame in case the state becomes dirty again schedule_frame_and_commit(surface); surface->dirty = false; } static void frame_handle_done(void *data, struct wl_callback *callback, uint32_t time) { struct mako_surface *surface = data; if (surface->frame_callback) { wl_callback_destroy(surface->frame_callback); surface->frame_callback = NULL; } // Only draw again if we need to if (surface->dirty) { send_frame(surface); } } static const struct wl_callback_listener frame_listener = { .done = frame_handle_done, }; static void schedule_frame_and_commit(struct mako_surface *surface) { if (surface->frame_callback) { return; } if (surface->surface == NULL) { // We don't yet have a surface, create it immediately send_frame(surface); return; } surface->frame_callback = wl_surface_frame(surface->surface); wl_callback_add_listener(surface->frame_callback, &frame_listener, surface); wl_surface_commit(surface->surface); } void set_dirty(struct mako_surface *surface) { if (surface->dirty) { return; } surface->dirty = true; schedule_frame_and_commit(surface); } static void activation_token_handle_done(void *data, struct xdg_activation_token_v1 *token, const char *token_str) { char **out = data; *out = strdup(token_str); } static const struct xdg_activation_token_v1_listener activation_token_listener = { .done = activation_token_handle_done, }; char *create_xdg_activation_token(struct mako_surface *surface, struct mako_seat *seat, uint32_t serial) { struct mako_state *state = seat->state; if (state->xdg_activation == NULL) { return NULL; } char *token_str = NULL; struct xdg_activation_token_v1 *token = xdg_activation_v1_get_activation_token(state->xdg_activation); xdg_activation_token_v1_add_listener(token, &activation_token_listener, &token_str); xdg_activation_token_v1_set_serial(token, serial, seat->wl_seat); xdg_activation_token_v1_set_surface(token, surface->surface); xdg_activation_token_v1_commit(token); while (wl_display_dispatch(state->display) >= 0 && token_str == NULL) { // This space is intentionally left blank } xdg_activation_token_v1_destroy(token); return token_str; }