From c63d35c9420bf0bdb7849dc00a2d679e9ac57524 Mon Sep 17 00:00:00 2001 From: Johan Malm Date: Mon, 28 Apr 2025 20:38:19 +0100 Subject: [PATCH] Add labnag Based on swaynag (https://github.com/swaywm/sway/tree/master/swaynag) Copied at commit: https://github.com/swaywm/sway/commit/03483ff3707a358d935e451d39748e58c205ce8a Contains the following modifiations: - Some functional changes including: - Disable exclusive-zone by default (Written-by: @Consolatis) and add command line option -x|--exclusive-zone - Add close timeout (Written-by: @Consolatis) and -t|--timeout option - Use index of button (from right-to-left) for exit code - Disable reading from config file and remove associated --type option - Refactoring including: - Use wlr_log() instead of the log.{c,h} functions - Use wl_list instead of sway's list.c implementation - In the pango wrapper functions, use glib's g_strdup_vprintf() rather than the original stringop.c functions - Align with labwc coding style to pass checkpatch.pl - Re-licenced from MIT to GPL-2.0, and add Copyright notices for original authors v2 - Remove option -s|--dismiss-button and the default "X" button. To get such a button, "-Z X :" - Remove options -b and -z because there is no requirement to run in a terminal. - Remove *-no-terminal from options --button and --button-dismiss because commands are now always run directly without a terminal. v3 - Allow -B/-Z options without action-argument - Invert button order of -B/-Z so that `labnag -m foo -Z x -Z y -Z z` results in three buttons with "x" furthest to the left, and "z" on the right (rather than the other way around). - Use signalfd() to prevent race conditions on SIGTERM v4 - Limit number of stdin lines to 200 to avoid hogging CPU Co-Authored-by: tokyo4j --- clients/labnag.c | 1652 +++++++++++++++++++++++++++++++++++++++++ clients/meson.build | 47 ++ clients/pool-buffer.c | 145 ++++ clients/pool-buffer.h | 30 + docs/labnag.1.scd | 109 +++ docs/meson.build | 1 + meson.build | 1 + scripts/check | 2 +- 8 files changed, 1986 insertions(+), 1 deletion(-) create mode 100644 clients/labnag.c create mode 100644 clients/meson.build create mode 100644 clients/pool-buffer.c create mode 100644 clients/pool-buffer.h create mode 100644 docs/labnag.1.scd diff --git a/clients/labnag.c b/clients/labnag.c new file mode 100644 index 00000000..514ac864 --- /dev/null +++ b/clients/labnag.c @@ -0,0 +1,1652 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Based on https://github.com/swaywm/sway/tree/master/swaynag + * + * Copyright (C) 2016-2017 Drew DeVault + * Copyright (C) 2025 Johan Malm + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pool-buffer.h" +#include "cursor-shape-v1-client-protocol.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +#define LABNAG_MAX_HEIGHT 500 +#define LAB_EXIT_FAILURE 255 +#define LAB_EXIT_SUCCESS 0 + +struct conf { + PangoFontDescription *font_description; + char *output; + uint32_t anchors; + int32_t layer; /* enum zwlr_layer_shell_v1_layer or -1 if unset */ + + /* Colors */ + uint32_t button_text; + uint32_t button_background; + uint32_t details_background; + uint32_t background; + uint32_t text; + uint32_t border; + uint32_t border_bottom; + + /* Sizing */ + ssize_t bar_border_thickness; + ssize_t message_padding; + ssize_t details_border_thickness; + ssize_t button_border_thickness; + ssize_t button_gap; + ssize_t button_gap_close; + ssize_t button_margin_right; + ssize_t button_padding; +}; + +struct nag; + +struct pointer { + struct wl_pointer *pointer; + uint32_t serial; + struct wl_cursor_theme *cursor_theme; + struct wl_cursor_image *cursor_image; + struct wl_surface *cursor_surface; + int x; + int y; +}; + +struct seat { + struct wl_seat *wl_seat; + uint32_t wl_name; + struct nag *nag; + struct pointer pointer; + struct wl_list link; /* nag.seats */ +}; + +struct output { + char *name; + struct wl_output *wl_output; + uint32_t wl_name; + uint32_t scale; + struct nag *nag; + struct wl_list link; /* nag.outputs */ +}; + +struct button { + char *text; + char *action; + int x; + int y; + int width; + int height; + bool expand; + bool dismiss; + struct wl_list link; +}; + +enum { + FD_WAYLAND, + FD_TIMER, + FD_SIGNAL, + + NR_FDS, +}; + +struct nag { + bool run_display; + + struct wl_display *display; + struct wl_compositor *compositor; + struct wl_seat *seat; + struct wl_shm *shm; + struct wl_list outputs; + struct wl_list seats; + struct output *output; + struct zwlr_layer_shell_v1 *layer_shell; + struct zwlr_layer_surface_v1 *layer_surface; + struct wp_cursor_shape_manager_v1 *cursor_shape_manager; + struct wl_surface *surface; + + uint32_t width; + uint32_t height; + int32_t scale; + struct pool_buffer buffers[2]; + struct pool_buffer *current_buffer; + + struct conf *conf; + char *message; + struct wl_list buttons; + struct pollfd pollfds[NR_FDS]; + + struct { + bool visible; + char *message; + char *details_text; + int close_timeout; + bool use_exclusive_zone; + + int x; + int y; + int width; + int height; + + int offset; + int visible_lines; + int total_lines; + struct button *button_details; + struct button button_up; + struct button button_down; + } details; +}; + +static int exit_status = LAB_EXIT_FAILURE; + +static void close_pollfd(struct pollfd *pollfd); + +static PangoLayout * +get_pango_layout(cairo_t *cairo, const PangoFontDescription *desc, + const char *text, double scale, bool markup) +{ + PangoLayout *layout = pango_cairo_create_layout(cairo); + pango_context_set_round_glyph_positions(pango_layout_get_context(layout), false); + + PangoAttrList *attrs; + if (markup) { + char *buf; + GError *error = NULL; + if (pango_parse_markup(text, -1, 0, &attrs, &buf, NULL, &error)) { + pango_layout_set_text(layout, buf, -1); + free(buf); + } else { + wlr_log(WLR_ERROR, "pango_parse_markup '%s' -> error %s", + text, error->message); + g_error_free(error); + markup = false; /* fallback to plain text */ + } + } + if (!markup) { + attrs = pango_attr_list_new(); + pango_layout_set_text(layout, text, -1); + } + + pango_attr_list_insert(attrs, pango_attr_scale_new(scale)); + pango_layout_set_font_description(layout, desc); + pango_layout_set_single_paragraph_mode(layout, 1); + pango_layout_set_attributes(layout, attrs); + pango_attr_list_unref(attrs); + return layout; +} + +static void +get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, int *height, + int *baseline, double scale, bool markup, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + gchar *buf = g_strdup_vprintf(fmt, args); + va_end(args); + if (!buf) { + return; + } + + PangoLayout *layout = get_pango_layout(cairo, desc, buf, scale, markup); + pango_cairo_update_layout(cairo, layout); + pango_layout_get_pixel_size(layout, width, height); + if (baseline) { + *baseline = pango_layout_get_baseline(layout) / PANGO_SCALE; + } + g_object_unref(layout); + + g_free(buf); +} + +static void +render_text(cairo_t *cairo, const PangoFontDescription *desc, double scale, + bool markup, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + gchar *buf = g_strdup_vprintf(fmt, args); + va_end(args); + if (!buf) { + return; + } + + PangoLayout *layout = get_pango_layout(cairo, desc, buf, scale, markup); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_get_font_options(cairo, fo); + pango_cairo_context_set_font_options(pango_layout_get_context(layout), fo); + cairo_font_options_destroy(fo); + pango_cairo_update_layout(cairo, layout); + pango_cairo_show_layout(cairo, layout); + g_object_unref(layout); + + g_free(buf); +} + +static void +cairo_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 uint32_t +render_message(cairo_t *cairo, struct nag *nag) +{ + int text_width, text_height; + get_text_size(cairo, nag->conf->font_description, &text_width, + &text_height, NULL, 1, true, "%s", nag->message); + + int padding = nag->conf->message_padding; + + uint32_t ideal_height = text_height + padding * 2; + uint32_t ideal_surface_height = ideal_height; + if (nag->height < ideal_surface_height) { + return ideal_surface_height; + } + + cairo_set_source_u32(cairo, nag->conf->text); + cairo_move_to(cairo, padding, (int)(ideal_height - text_height) / 2); + render_text(cairo, nag->conf->font_description, 1, false, + "%s", nag->message); + + return ideal_surface_height; +} + +static void +render_details_scroll_button(cairo_t *cairo, struct nag *nag, + struct button *button) +{ + int text_width, text_height; + get_text_size(cairo, nag->conf->font_description, &text_width, + &text_height, NULL, 1, true, "%s", button->text); + + int border = nag->conf->button_border_thickness; + int padding = nag->conf->button_padding; + + cairo_set_source_u32(cairo, nag->conf->details_background); + cairo_rectangle(cairo, button->x, button->y, + button->width, button->height); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, nag->conf->button_background); + cairo_rectangle(cairo, button->x + border, button->y + border, + button->width - (border * 2), + button->height - (border * 2)); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, nag->conf->button_text); + cairo_move_to(cairo, button->x + border + padding, + button->y + border + (button->height - text_height) / 2); + render_text(cairo, nag->conf->font_description, 1, true, + "%s", button->text); +} + +static int +get_detailed_scroll_button_width(cairo_t *cairo, struct nag *nag) +{ + int up_width, down_width, temp_height; + get_text_size(cairo, nag->conf->font_description, &up_width, &temp_height, + NULL, 1, true, "%s", nag->details.button_up.text); + get_text_size(cairo, nag->conf->font_description, &down_width, &temp_height, + NULL, 1, true, "%s", nag->details.button_down.text); + + int text_width = up_width > down_width ? up_width : down_width; + int border = nag->conf->button_border_thickness; + int padding = nag->conf->button_padding; + + return text_width + border * 2 + padding * 2; +} + +static uint32_t +render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) +{ + uint32_t width = nag->width; + + int border = nag->conf->details_border_thickness; + int padding = nag->conf->message_padding; + int decor = padding + border; + + nag->details.x = decor; + nag->details.y = y + decor; + nag->details.width = width - decor * 2; + + PangoLayout *layout = get_pango_layout(cairo, nag->conf->font_description, + nag->details.message, 1, false); + pango_layout_set_width(layout, + (nag->details.width - padding * 2) * PANGO_SCALE); + pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); + pango_layout_set_single_paragraph_mode(layout, false); + pango_cairo_update_layout(cairo, layout); + nag->details.total_lines = pango_layout_get_line_count(layout); + + PangoLayoutLine *line; + line = pango_layout_get_line_readonly(layout, nag->details.offset); + gint offset = line->start_index; + const char *text = pango_layout_get_text(layout); + pango_layout_set_text(layout, text + offset, strlen(text) - offset); + + int text_width, text_height; + pango_cairo_update_layout(cairo, layout); + pango_layout_get_pixel_size(layout, &text_width, &text_height); + + bool show_buttons = nag->details.offset > 0; + int button_width = get_detailed_scroll_button_width(cairo, nag); + if (show_buttons) { + nag->details.width -= button_width; + pango_layout_set_width(layout, + (nag->details.width - padding * 2) * PANGO_SCALE); + } + + uint32_t ideal_height; + do { + ideal_height = nag->details.y + text_height + decor + padding * 2; + if (ideal_height > LABNAG_MAX_HEIGHT) { + ideal_height = LABNAG_MAX_HEIGHT; + + if (!show_buttons) { + show_buttons = true; + nag->details.width -= button_width; + pango_layout_set_width(layout, + (nag->details.width - padding * 2) * PANGO_SCALE); + } + } + + nag->details.height = ideal_height - nag->details.y - decor; + pango_layout_set_height(layout, + (nag->details.height - padding * 2) * PANGO_SCALE); + pango_layout_set_ellipsize(layout, PANGO_ELLIPSIZE_END); + pango_cairo_update_layout(cairo, layout); + pango_layout_get_pixel_size(layout, &text_width, &text_height); + } while (text_height != (nag->details.height - padding * 2)); + + nag->details.visible_lines = pango_layout_get_line_count(layout); + + if (show_buttons) { + nag->details.button_up.x = nag->details.x + nag->details.width; + nag->details.button_up.y = nag->details.y; + nag->details.button_up.width = button_width; + nag->details.button_up.height = nag->details.height / 2; + render_details_scroll_button(cairo, nag, &nag->details.button_up); + + nag->details.button_down.x = nag->details.x + nag->details.width; + nag->details.button_down.y = + nag->details.button_up.y + nag->details.button_up.height; + nag->details.button_down.width = button_width; + nag->details.button_down.height = nag->details.height / 2; + render_details_scroll_button(cairo, nag, &nag->details.button_down); + } + + cairo_set_source_u32(cairo, nag->conf->details_background); + cairo_rectangle(cairo, nag->details.x, nag->details.y, + nag->details.width, nag->details.height); + cairo_fill(cairo); + + cairo_move_to(cairo, nag->details.x + padding, nag->details.y + padding); + cairo_set_source_u32(cairo, nag->conf->text); + pango_cairo_show_layout(cairo, layout); + g_object_unref(layout); + + return ideal_height; +} + +static uint32_t +render_button(cairo_t *cairo, struct nag *nag, struct button *button, int *x) +{ + int text_width, text_height; + get_text_size(cairo, nag->conf->font_description, &text_width, + &text_height, NULL, 1, true, "%s", button->text); + + int border = nag->conf->button_border_thickness; + int padding = nag->conf->button_padding; + + uint32_t ideal_height = text_height + padding * 2 + border * 2; + uint32_t ideal_surface_height = ideal_height; + if (nag->height < ideal_surface_height) { + return ideal_surface_height; + } + + button->x = *x - border - text_width - padding * 2 + 1; + button->y = (int)(ideal_height - text_height) / 2 - padding + 1; + button->width = text_width + padding * 2; + button->height = text_height + padding * 2; + + cairo_set_source_u32(cairo, nag->conf->border); + cairo_rectangle(cairo, button->x - border, button->y - border, + button->width + border * 2, button->height + border * 2); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, nag->conf->button_background); + cairo_rectangle(cairo, button->x, button->y, + button->width, button->height); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, nag->conf->button_text); + cairo_move_to(cairo, button->x + padding, button->y + padding); + render_text(cairo, nag->conf->font_description, 1, true, + "%s", button->text); + + *x = button->x - border; + + return ideal_surface_height; +} + +static uint32_t +render_to_cairo(cairo_t *cairo, struct nag *nag) +{ + uint32_t max_height = 0; + + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + cairo_set_source_u32(cairo, nag->conf->background); + cairo_paint(cairo); + + uint32_t h = render_message(cairo, nag); + max_height = h > max_height ? h : max_height; + + int x = nag->width - nag->conf->button_margin_right; + x -= nag->conf->button_gap_close; + + struct button *button; + wl_list_for_each(button, &nag->buttons, link) { + h = render_button(cairo, nag, button, &x); + max_height = h > max_height ? h : max_height; + x -= nag->conf->button_gap; + } + + if (nag->details.visible) { + h = render_detailed(cairo, nag, max_height); + max_height = h > max_height ? h : max_height; + } + + int border = nag->conf->bar_border_thickness; + if (max_height > nag->height) { + max_height += border; + } + cairo_set_source_u32(cairo, nag->conf->border_bottom); + cairo_rectangle(cairo, 0, + nag->height - border, + nag->width, + border); + cairo_fill(cairo); + + return max_height; +} + +static void +render_frame(struct nag *nag) +{ + if (!nag->run_display) { + return; + } + + cairo_surface_t *recorder = cairo_recording_surface_create( + CAIRO_CONTENT_COLOR_ALPHA, NULL); + cairo_t *cairo = cairo_create(recorder); + cairo_scale(cairo, nag->scale, nag->scale); + cairo_save(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(cairo); + cairo_restore(cairo); + uint32_t height = render_to_cairo(cairo, nag); + if (height != nag->height) { + zwlr_layer_surface_v1_set_size(nag->layer_surface, 0, height); + if (nag->details.use_exclusive_zone) { + zwlr_layer_surface_v1_set_exclusive_zone( + nag->layer_surface, height); + } + wl_surface_commit(nag->surface); + wl_display_roundtrip(nag->display); + } else { + nag->current_buffer = get_next_buffer(nag->shm, + nag->buffers, + nag->width * nag->scale, + nag->height * nag->scale); + if (!nag->current_buffer) { + wlr_log(WLR_DEBUG, "Failed to get buffer. Skipping frame."); + goto cleanup; + } + + cairo_t *shm = nag->current_buffer->cairo; + cairo_save(shm); + cairo_set_operator(shm, CAIRO_OPERATOR_CLEAR); + cairo_paint(shm); + cairo_restore(shm); + cairo_set_source_surface(shm, recorder, 0.0, 0.0); + cairo_paint(shm); + + wl_surface_set_buffer_scale(nag->surface, nag->scale); + wl_surface_attach(nag->surface, + nag->current_buffer->buffer, 0, 0); + wl_surface_damage(nag->surface, 0, 0, + nag->width, nag->height); + wl_surface_commit(nag->surface); + wl_display_roundtrip(nag->display); + } + +cleanup: + cairo_surface_destroy(recorder); + cairo_destroy(cairo); +} + +static void +seat_destroy(struct seat *seat) +{ + if (seat->pointer.cursor_theme) { + wl_cursor_theme_destroy(seat->pointer.cursor_theme); + } + if (seat->pointer.pointer) { + wl_pointer_destroy(seat->pointer.pointer); + } + wl_seat_destroy(seat->wl_seat); + wl_list_remove(&seat->link); + free(seat); +} + +static void +nag_destroy(struct nag *nag) +{ + nag->run_display = false; + + struct button *button, *next; + wl_list_for_each_safe(button, next, &nag->buttons, link) { + wl_list_remove(&button->link); + free(button); + } + free(nag->details.message); + + pango_font_description_free(nag->conf->font_description); + + if (nag->layer_surface) { + zwlr_layer_surface_v1_destroy(nag->layer_surface); + } + + if (nag->surface) { + wl_surface_destroy(nag->surface); + } + + if (nag->layer_shell) { + zwlr_layer_shell_v1_destroy(nag->layer_shell); + } + + if (nag->cursor_shape_manager) { + wp_cursor_shape_manager_v1_destroy(nag->cursor_shape_manager); + } + + struct seat *seat, *tmpseat; + wl_list_for_each_safe(seat, tmpseat, &nag->seats, link) { + seat_destroy(seat); + } + + destroy_buffer(&nag->buffers[0]); + destroy_buffer(&nag->buffers[1]); + + if (nag->outputs.prev || nag->outputs.next) { + struct output *output, *temp; + wl_list_for_each_safe(output, temp, &nag->outputs, link) { + wl_output_destroy(output->wl_output); + free(output->name); + wl_list_remove(&output->link); + free(output); + }; + } + + if (nag->compositor) { + wl_compositor_destroy(nag->compositor); + } + + if (nag->shm) { + wl_shm_destroy(nag->shm); + } + + if (nag->display) { + wl_display_disconnect(nag->display); + } + pango_cairo_font_map_set_default(NULL); + + close_pollfd(&nag->pollfds[FD_TIMER]); + close_pollfd(&nag->pollfds[FD_SIGNAL]); +} + +static void +button_execute(struct nag *nag, struct button *button) +{ + wlr_log(WLR_DEBUG, "Executing [%s]: %s", button->text, button->action); + if (button->expand) { + nag->details.visible = !nag->details.visible; + render_frame(nag); + return; + } + if (button->dismiss) { + nag->run_display = false; + } + if (button->action) { + pid_t pid = fork(); + if (pid < 0) { + wlr_log_errno(WLR_DEBUG, "Failed to fork"); + return; + } else if (pid == 0) { + /* + * Child process. Will be used to prevent zombie + * processes + */ + pid = fork(); + if (pid < 0) { + wlr_log_errno(WLR_DEBUG, "Failed to fork"); + return; + } else if (pid == 0) { + /* + * Child of the child. Will be reparented to the + * init process + */ + execlp("sh", "sh", "-c", button->action, NULL); + wlr_log_errno(WLR_DEBUG, "execlp failed"); + _exit(LAB_EXIT_FAILURE); + } + _exit(EXIT_SUCCESS); + } + + if (waitpid(pid, NULL, 0) < 0) { + wlr_log_errno(WLR_DEBUG, "waitpid failed"); + } + } +} + +static void +layer_surface_configure(void *data, struct zwlr_layer_surface_v1 *surface, + uint32_t serial, uint32_t width, uint32_t height) +{ + struct nag *nag = data; + nag->width = width; + nag->height = height; + zwlr_layer_surface_v1_ack_configure(surface, serial); + render_frame(nag); +} + +static void +layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) +{ + struct nag *nag = data; + nag_destroy(nag); +} + +static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + +static void +surface_enter(void *data, struct wl_surface *surface, struct wl_output *output) +{ + struct nag *nag = data; + struct output *nag_output; + wl_list_for_each(nag_output, &nag->outputs, link) { + if (nag_output->wl_output == output) { + wlr_log(WLR_DEBUG, "Surface enter on output %s", + nag_output->name); + nag->output = nag_output; + nag->scale = nag->output->scale; + render_frame(nag); + break; + } + } +} + +static void +surface_leave(void *data, struct wl_surface *wl_surface, struct wl_output *output) +{ + /* nop */ +} + +static const struct wl_surface_listener surface_listener = { + .enter = surface_enter, + .leave = surface_leave, +}; + +static void +update_cursor(struct seat *seat) +{ + struct pointer *pointer = &seat->pointer; + struct nag *nag = seat->nag; + if (pointer->cursor_theme) { + wl_cursor_theme_destroy(pointer->cursor_theme); + } + const char *cursor_theme = getenv("XCURSOR_THEME"); + unsigned int cursor_size = 24; + const char *env_cursor_size = getenv("XCURSOR_SIZE"); + if (env_cursor_size && *env_cursor_size) { + errno = 0; + char *end; + unsigned int size = strtoul(env_cursor_size, &end, 10); + if (!*end && errno == 0) { + cursor_size = size; + } + } + pointer->cursor_theme = wl_cursor_theme_load( + cursor_theme, cursor_size * nag->scale, nag->shm); + if (!pointer->cursor_theme) { + wlr_log(WLR_ERROR, "Failed to load cursor theme"); + return; + } + struct wl_cursor *cursor = + wl_cursor_theme_get_cursor(pointer->cursor_theme, "default"); + if (!cursor) { + wlr_log(WLR_ERROR, "Failed to get default cursor from theme"); + return; + } + pointer->cursor_image = cursor->images[0]; + wl_surface_set_buffer_scale(pointer->cursor_surface, + nag->scale); + wl_surface_attach(pointer->cursor_surface, + wl_cursor_image_get_buffer(pointer->cursor_image), 0, 0); + wl_pointer_set_cursor(pointer->pointer, pointer->serial, + pointer->cursor_surface, + pointer->cursor_image->hotspot_x / nag->scale, + pointer->cursor_image->hotspot_y / nag->scale); + wl_surface_damage_buffer(pointer->cursor_surface, 0, 0, + INT32_MAX, INT32_MAX); + wl_surface_commit(pointer->cursor_surface); +} + +static void +update_all_cursors(struct nag *nag) +{ + struct seat *seat; + wl_list_for_each(seat, &nag->seats, link) { + if (seat->pointer.pointer) { + update_cursor(seat); + } + } +} + +static void +wl_pointer_enter(void *data, struct wl_pointer *wl_pointer, uint32_t serial, + struct wl_surface *surface, wl_fixed_t surface_x, + wl_fixed_t surface_y) +{ + struct seat *seat = data; + + struct pointer *pointer = &seat->pointer; + pointer->x = wl_fixed_to_int(surface_x); + pointer->y = wl_fixed_to_int(surface_y); + + if (seat->nag->cursor_shape_manager) { + struct wp_cursor_shape_device_v1 *device = + wp_cursor_shape_manager_v1_get_pointer( + seat->nag->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 { + pointer->serial = serial; + update_cursor(seat); + } +} + +static void +wl_pointer_leave(void *data, struct wl_pointer *wl_pointer, + uint32_t serial, struct wl_surface *surface) +{ + /* nop */ +} + +static void +wl_pointer_motion(void *data, struct wl_pointer *wl_pointer, uint32_t time, + wl_fixed_t surface_x, wl_fixed_t surface_y) +{ + struct seat *seat = data; + seat->pointer.x = wl_fixed_to_int(surface_x); + seat->pointer.y = wl_fixed_to_int(surface_y); +} + +static void +wl_pointer_button(void *data, struct wl_pointer *wl_pointer, uint32_t serial, + uint32_t time, uint32_t button, uint32_t state) +{ + struct seat *seat = data; + struct nag *nag = seat->nag; + + if (state != WL_POINTER_BUTTON_STATE_PRESSED) { + return; + } + + double x = seat->pointer.x; + double y = seat->pointer.y; + + int index = 0; + struct button *nagbutton; + wl_list_for_each(nagbutton, &nag->buttons, link) { + if (x >= nagbutton->x + && y >= nagbutton->y + && x < nagbutton->x + nagbutton->width + && y < nagbutton->y + nagbutton->height) { + button_execute(nag, nagbutton); + exit_status = index; + return; + } + ++index; + } + + if (nag->details.visible && + nag->details.total_lines != nag->details.visible_lines) { + struct button button_up = nag->details.button_up; + if (x >= button_up.x + && y >= button_up.y + && x < button_up.x + button_up.width + && y < button_up.y + button_up.height + && nag->details.offset > 0) { + nag->details.offset--; + render_frame(nag); + return; + } + + struct button button_down = nag->details.button_down; + int bot = nag->details.total_lines; + bot -= nag->details.visible_lines; + if (x >= button_down.x + && y >= button_down.y + && x < button_down.x + button_down.width + && y < button_down.y + button_down.height + && nag->details.offset < bot) { + nag->details.offset++; + render_frame(nag); + return; + } + } +} + +static void +wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, uint32_t time, + uint32_t axis, wl_fixed_t value) +{ + struct seat *seat = data; + struct nag *nag = seat->nag; + if (!nag->details.visible + || seat->pointer.x < nag->details.x + || seat->pointer.y < nag->details.y + || seat->pointer.x >= nag->details.x + nag->details.width + || seat->pointer.y >= nag->details.y + nag->details.height + || nag->details.total_lines == nag->details.visible_lines) { + return; + } + + int direction = wl_fixed_to_int(value); + int bot = nag->details.total_lines - nag->details.visible_lines; + if (direction < 0 && nag->details.offset > 0) { + nag->details.offset--; + } else if (direction > 0 && nag->details.offset < bot) { + nag->details.offset++; + } + + render_frame(nag); +} + +static void +wl_pointer_frame(void *data, struct wl_pointer *wl_pointer) +{ + struct seat *seat = data; + /* pointer inputs clears timer for auto-closing */ + close_pollfd(&seat->nag->pollfds[FD_TIMER]); +} + +static void +wl_pointer_axis_source(void *data, struct wl_pointer *wl_pointer, + uint32_t axis_source) +{ + /* nop */ +} + +static void +wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis) +{ + /* nop */ +} + +static void +wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer, + uint32_t axis, int32_t discrete) +{ + /* nop */ +} + +static const struct wl_pointer_listener pointer_listener = { + .enter = wl_pointer_enter, + .leave = wl_pointer_leave, + .motion = wl_pointer_motion, + .button = wl_pointer_button, + .axis = wl_pointer_axis, + .frame = wl_pointer_frame, + .axis_source = wl_pointer_axis_source, + .axis_stop = wl_pointer_axis_stop, + .axis_discrete = wl_pointer_axis_discrete, +}; + +static void +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, + enum wl_seat_capability caps) +{ + struct seat *seat = data; + bool cap_pointer = caps & WL_SEAT_CAPABILITY_POINTER; + if (cap_pointer && !seat->pointer.pointer) { + seat->pointer.pointer = wl_seat_get_pointer(wl_seat); + wl_pointer_add_listener(seat->pointer.pointer, + &pointer_listener, seat); + } else if (!cap_pointer && seat->pointer.pointer) { + wl_pointer_destroy(seat->pointer.pointer); + seat->pointer.pointer = NULL; + } +} + +static void +seat_handle_name(void *data, struct wl_seat *wl_seat, const char *name) +{ + /* nop */ +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +static void +output_geometry(void *data, struct wl_output *wl_output, int32_t x, int32_t y, + int32_t physical_width, int32_t physical_height, + int32_t subpixel, const char *make, const char *model, + int32_t transform) +{ + /* nop */ +} + +static void +output_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) +{ + /* nop */ +} + +static void +output_done(void *data, struct wl_output *output) +{ + /* nop */ +} + +static void +output_scale(void *data, struct wl_output *output, int32_t factor) +{ + struct output *nag_output = data; + nag_output->scale = factor; + if (nag_output->nag->output == nag_output) { + nag_output->nag->scale = nag_output->scale; + if (!nag_output->nag->cursor_shape_manager) { + update_all_cursors(nag_output->nag); + } + render_frame(nag_output->nag); + } +} + +static void +output_name(void *data, struct wl_output *output, const char *name) +{ + struct output *nag_output = data; + nag_output->name = strdup(name); + + const char *outname = nag_output->nag->conf->output; + if (!nag_output->nag->output && outname && + strcmp(outname, name) == 0) { + wlr_log(WLR_DEBUG, "Using output %s", name); + nag_output->nag->output = nag_output; + } +} + +static void +output_description(void *data, struct wl_output *wl_output, + const char *description) +{ + /* nop */ +} + +static const struct wl_output_listener output_listener = { + .geometry = output_geometry, + .mode = output_mode, + .done = output_done, + .scale = output_scale, + .name = output_name, + .description = output_description, +}; + +static void +handle_global(void *data, struct wl_registry *registry, uint32_t name, + const char *interface, uint32_t version) +{ + struct nag *nag = data; + if (strcmp(interface, wl_compositor_interface.name) == 0) { + nag->compositor = wl_registry_bind(registry, name, + &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + struct seat *seat = calloc(1, sizeof(*seat)); + if (!seat) { + perror("calloc"); + return; + } + + seat->nag = nag; + seat->wl_name = name; + seat->wl_seat = + wl_registry_bind(registry, name, &wl_seat_interface, 5); + + wl_seat_add_listener(seat->wl_seat, &seat_listener, seat); + + wl_list_insert(&nag->seats, &seat->link); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + nag->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, wl_output_interface.name) == 0) { + if (!nag->output) { + struct output *output = calloc(1, sizeof(*output)); + if (!output) { + perror("calloc"); + return; + } + output->wl_output = wl_registry_bind(registry, name, + &wl_output_interface, 4); + output->wl_name = name; + output->scale = 1; + output->nag = nag; + wl_list_insert(&nag->outputs, &output->link); + wl_output_add_listener(output->wl_output, + &output_listener, output); + } + } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + nag->layer_shell = wl_registry_bind( + registry, name, &zwlr_layer_shell_v1_interface, 1); + } else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) { + nag->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 nag *nag = data; + if (nag->output->wl_name == name) { + nag->run_display = false; + } + + struct seat *seat, *tmpseat; + wl_list_for_each_safe(seat, tmpseat, &nag->seats, link) { + if (seat->wl_name == name) { + seat_destroy(seat); + } + } +} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + +static void +nag_setup_cursors(struct nag *nag) +{ + struct seat *seat; + + wl_list_for_each(seat, &nag->seats, link) { + struct pointer *p = &seat->pointer; + + p->cursor_surface = + wl_compositor_create_surface(nag->compositor); + assert(p->cursor_surface); + } +} + +static void +nag_setup(struct nag *nag) +{ + nag->display = wl_display_connect(NULL); + if (!nag->display) { + wlr_log(WLR_ERROR, "Unable to connect to the compositor. " + "If your compositor is running, check or set the " + "WAYLAND_DISPLAY environment variable."); + exit(LAB_EXIT_FAILURE); + } + + nag->scale = 1; + + struct wl_registry *registry = wl_display_get_registry(nag->display); + wl_registry_add_listener(registry, ®istry_listener, nag); + if (wl_display_roundtrip(nag->display) < 0) { + wlr_log(WLR_ERROR, "failed to register with the wayland display"); + exit(LAB_EXIT_FAILURE); + } + + assert(nag->compositor && nag->layer_shell && nag->shm); + + /* Second roundtrip to get wl_output properties */ + if (wl_display_roundtrip(nag->display) < 0) { + wlr_log(WLR_ERROR, "Error during outputs init."); + nag_destroy(nag); + exit(LAB_EXIT_FAILURE); + } + + if (!nag->output && nag->conf->output) { + wlr_log(WLR_ERROR, "Output '%s' not found", nag->conf->output); + nag_destroy(nag); + exit(LAB_EXIT_FAILURE); + } + + if (!nag->cursor_shape_manager) { + nag_setup_cursors(nag); + } + + nag->surface = wl_compositor_create_surface(nag->compositor); + assert(nag->surface); + wl_surface_add_listener(nag->surface, &surface_listener, nag); + + nag->layer_surface = zwlr_layer_shell_v1_get_layer_surface( + nag->layer_shell, nag->surface, + nag->output ? nag->output->wl_output : NULL, + nag->conf->layer, + "nag"); + assert(nag->layer_surface); + zwlr_layer_surface_v1_add_listener(nag->layer_surface, + &layer_surface_listener, nag); + zwlr_layer_surface_v1_set_anchor(nag->layer_surface, + nag->conf->anchors); + + wl_registry_destroy(registry); + + nag->pollfds[FD_WAYLAND].fd = wl_display_get_fd(nag->display); + nag->pollfds[FD_WAYLAND].events = POLLIN; + + if (nag->details.close_timeout != 0) { + nag->pollfds[FD_TIMER].fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + nag->pollfds[FD_TIMER].events = POLLIN; + struct itimerspec timeout = { + .it_value.tv_sec = nag->details.close_timeout, + }; + timerfd_settime(nag->pollfds[FD_TIMER].fd, 0, &timeout, NULL); + } else { + nag->pollfds[FD_TIMER].fd = -1; + } + + sigset_t mask; + sigaddset(&mask, SIGINT); + sigaddset(&mask, SIGTERM); + sigprocmask(SIG_BLOCK, &mask, NULL); + nag->pollfds[FD_SIGNAL].fd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK); + nag->pollfds[FD_SIGNAL].events = POLLIN; +} + +static void +close_pollfd(struct pollfd *pollfd) +{ + if (pollfd->fd == -1) { + return; + } + close(pollfd->fd); + pollfd->fd = -1; + pollfd->events = 0; + pollfd->revents = 0; +} + +static void +nag_run(struct nag *nag) +{ + nag->run_display = true; + render_frame(nag); + while (nag->run_display) { + while (wl_display_prepare_read(nag->display) != 0) { + wl_display_dispatch_pending(nag->display); + } + + errno = 0; + if (wl_display_flush(nag->display) == -1 && errno != EAGAIN) { + break; + } + + if (!nag->run_display) { + break; + } + + poll(nag->pollfds, NR_FDS, -1); + if (nag->pollfds[FD_WAYLAND].revents & POLLIN) { + wl_display_read_events(nag->display); + } else { + wl_display_cancel_read(nag->display); + } + if (nag->pollfds[FD_TIMER].revents & POLLIN) { + break; + } + if (nag->pollfds[FD_SIGNAL].revents & POLLIN) { + break; + } + } +} + +static void +conf_init(struct conf *conf) +{ + conf->font_description = pango_font_description_from_string("pango:Sans 10"); + conf->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP + | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT + | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + conf->button_background = 0x333333FF; + conf->details_background = 0x333333FF; + conf->background = 0x323232FF; + conf->text = 0xFFFFFFFF; + conf->button_text = 0xFFFFFFFF; + conf->border = 0x222222FF; + conf->border_bottom = 0x444444FF; + conf->bar_border_thickness = 2; + conf->message_padding = 8; + conf->details_border_thickness = 3; + conf->button_border_thickness = 3; + conf->button_gap = 20; + conf->button_gap_close = 15; + conf->button_margin_right = 2; + conf->button_padding = 3; + conf->button_background = 0x680A0AFF; + conf->details_background = 0x680A0AFF; + conf->background = 0x900000FF; + conf->text = 0xFFFFFFFF; + conf->button_text = 0xFFFFFFFF; + conf->border = 0xD92424FF; + conf->border_bottom = 0x470909FF; +} + +static bool +parse_color(const char *color, uint32_t *result) +{ + if (color[0] == '#') { + ++color; + } + int len = strlen(color); + if ((len != 6 && len != 8) || !isxdigit(color[0]) || !isxdigit(color[1])) { + return false; + } + char *ptr; + uint32_t parsed = strtoul(color, &ptr, 16); + if (*ptr != '\0') { + return false; + } + *result = len == 6 ? ((parsed << 8) | 0xFF) : parsed; + return true; +} + +/* + * As labnag is slow for large "detailed messages" we curtail stdin at an + * arbitrary size to avoid hogging the CPU. + */ +#define MAX_STDIN_LINES 200 +static char * +read_and_trim_stdin(void) +{ + char *buffer = NULL, *line = NULL; + size_t buffer_len = 0, line_size = 0; + int line_count = 0; + while (line_count < MAX_STDIN_LINES) { + ssize_t nread = getline(&line, &line_size, stdin); + if (nread == -1) { + if (feof(stdin)) { + break; + } else { + perror("getline"); + goto freeline; + } + } + buffer = realloc(buffer, buffer_len + nread + 1); + if (!buffer) { + perror("realloc"); + return NULL; + } + memcpy(&buffer[buffer_len], line, nread + 1); + buffer_len += nread; + ++line_count; + } + free(line); + + while (buffer_len && buffer[buffer_len - 1] == '\n') { + buffer[--buffer_len] = '\0'; + } + + return buffer; + +freeline: + free(line); + return NULL; +} + +static int +nag_parse_options(int argc, char **argv, struct nag *nag, + struct conf *conf, bool *debug) +{ + enum type_options { + TO_COLOR_BACKGROUND = 256, + TO_COLOR_BORDER, + TO_COLOR_BORDER_BOTTOM, + TO_COLOR_BUTTON, + TO_COLOR_DETAILS, + TO_COLOR_TEXT, + TO_COLOR_BUTTON_TEXT, + TO_THICK_BAR_BORDER, + TO_PADDING_MESSAGE, + TO_THICK_DET_BORDER, + TO_THICK_BTN_BORDER, + TO_GAP_BTN, + TO_GAP_BTN_DISMISS, + TO_MARGIN_BTN_RIGHT, + TO_PADDING_BTN, + }; + + static const struct option opts[] = { + {"button", required_argument, NULL, 'B'}, + {"button-dismiss", required_argument, NULL, 'Z'}, + {"debug", no_argument, NULL, 'd'}, + {"edge", required_argument, NULL, 'e'}, + {"layer", required_argument, NULL, 'y'}, + {"font", required_argument, NULL, 'f'}, + {"help", no_argument, NULL, 'h'}, + {"detailed-message", no_argument, NULL, 'l'}, + {"detailed-button", required_argument, NULL, 'L'}, + {"message", required_argument, NULL, 'm'}, + {"output", required_argument, NULL, 'o'}, + {"timeout", no_argument, NULL, 't'}, + {"version", no_argument, NULL, 'v'}, + + {"background", required_argument, NULL, TO_COLOR_BACKGROUND}, + {"border", required_argument, NULL, TO_COLOR_BORDER}, + {"border-bottom", required_argument, NULL, TO_COLOR_BORDER_BOTTOM}, + {"button-background", required_argument, NULL, TO_COLOR_BUTTON}, + {"text", required_argument, NULL, TO_COLOR_TEXT}, + {"button-text", required_argument, NULL, TO_COLOR_BUTTON_TEXT}, + {"border-bottom-size", required_argument, NULL, TO_THICK_BAR_BORDER}, + {"message-padding", required_argument, NULL, TO_PADDING_MESSAGE}, + {"details-border-size", required_argument, NULL, TO_THICK_DET_BORDER}, + {"details-background", required_argument, NULL, TO_COLOR_DETAILS}, + {"button-border-size", required_argument, NULL, TO_THICK_BTN_BORDER}, + {"button-gap", required_argument, NULL, TO_GAP_BTN}, + {"button-dismiss-gap", required_argument, NULL, TO_GAP_BTN_DISMISS}, + {"button-margin-right", required_argument, NULL, TO_MARGIN_BTN_RIGHT}, + {"button-padding", required_argument, NULL, TO_PADDING_BTN}, + + {0, 0, 0, 0} + }; + + const char *usage = + "Usage: nag [options...]\n" + "\n" + " -B, --button [] Create a button with text\n" + " -Z, --button-dismiss []\n" + " Like -B but dismiss nag when pressed\n" + " -d, --debug Enable debugging.\n" + " -e, --edge top|bottom Set the edge to use.\n" + " -y, --layer overlay|top|bottom|background\n" + " Set the layer to use.\n" + " -f, --font Set the font to use.\n" + " -h, --help Show help message and quit.\n" + " -l, --detailed-message Read a detailed message from stdin.\n" + " -L, --detailed-button Set the text of the detail button.\n" + " -m, --message Set the message text.\n" + " -o, --output Set the output to use.\n" + " -t, --timeout Set duration to close dialog.\n" + " -x, --exclusive-zone Use exclusive zone.\n" + " -v, --version Show the version number and quit.\n" + "\n" + "The following appearance options can also be given:\n" + " --background RRGGBB[AA] Background color.\n" + " --border RRGGBB[AA] Border color.\n" + " --border-bottom RRGGBB[AA] Bottom border color.\n" + " --button-background RRGGBB[AA] Button background color.\n" + " --text RRGGBB[AA] Text color.\n" + " --button-text RRGGBB[AA] Button text color.\n" + " --border-bottom-size size Thickness of the bar border.\n" + " --message-padding padding Padding for the message.\n" + " --details-border-size size Thickness for the details border.\n" + " --details-background RRGGBB[AA] Details background color.\n" + " --button-border-size size Thickness for the button border.\n" + " --button-gap gap Size of the gap between buttons\n" + " --button-dismiss-gap gap Size of the gap for dismiss button.\n" + " --button-margin-right margin Margin from dismiss button to edge.\n" + " --button-padding padding Padding for the button text.\n"; + + optind = 1; + while (1) { + int c = getopt_long(argc, argv, "B:Z:c:de:y:f:hlL:m:o:s:t:vx", opts, NULL); + if (c == -1) { + break; + } + switch (c) { + case 'B': /* Button */ + case 'Z': /* Button (Dismiss) */ { + struct button *button = calloc(1, sizeof(*button)); + if (!button) { + perror("calloc"); + return LAB_EXIT_FAILURE; + } + if (argv[optind] && argv[optind][0] != '-') { + button->action = argv[optind]; + optind++; + } + button->text = optarg; + button->dismiss = c == 'Z'; + wl_list_insert(&nag->buttons, &button->link); + break; + } + case 'd': /* Debug */ + *debug = true; + break; + case 'e': /* Edge */ + if (strcmp(optarg, "top") == 0) { + conf->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP + | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT + | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + } else if (strcmp(optarg, "bottom") == 0) { + conf->anchors = ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM + | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT + | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + } else { + fprintf(stderr, "Invalid edge: %s\n", optarg); + return LAB_EXIT_FAILURE; + } + break; + case 'y': /* Layer */ + if (strcmp(optarg, "background") == 0) { + conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND; + } else if (strcmp(optarg, "bottom") == 0) { + conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM; + } else if (strcmp(optarg, "top") == 0) { + conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_TOP; + } else if (strcmp(optarg, "overlay") == 0) { + conf->layer = ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + } else { + fprintf(stderr, "Invalid layer: %s\n" + "Usage: --layer overlay|top|bottom|background\n", + optarg); + return LAB_EXIT_FAILURE; + } + break; + case 'f': /* Font */ + pango_font_description_free(conf->font_description); + conf->font_description = pango_font_description_from_string(optarg); + break; + case 'l': /* Detailed Message */ + free(nag->details.message); + nag->details.message = read_and_trim_stdin(); + if (!nag->details.message) { + return LAB_EXIT_FAILURE; + } + nag->details.button_up.text = "▲"; + nag->details.button_down.text = "▼"; + break; + case 'L': /* Detailed Button Text */ + nag->details.details_text = optarg; + break; + case 'm': /* Message */ + nag->message = optarg; + break; + case 'o': /* Output */ + free(conf->output); + conf->output = optarg; + break; + case 't': + nag->details.close_timeout = atoi(optarg); + break; + case 'x': + nag->details.use_exclusive_zone = true; + break; + case 'v': /* Version */ + printf("labnag " LABWC_VERSION "\n"); + return LAB_EXIT_FAILURE; + case TO_COLOR_BACKGROUND: /* Background color */ + if (!parse_color(optarg, &conf->background)) { + fprintf(stderr, "Invalid background color: %s", optarg); + } + break; + case TO_COLOR_BORDER: /* Border color */ + if (!parse_color(optarg, &conf->border)) { + fprintf(stderr, "Invalid border color: %s", optarg); + } + break; + case TO_COLOR_BORDER_BOTTOM: /* Bottom border color */ + if (!parse_color(optarg, &conf->border_bottom)) { + fprintf(stderr, "Invalid border bottom color: %s", optarg); + } + break; + case TO_COLOR_BUTTON: /* Button background color */ + if (!parse_color(optarg, &conf->button_background)) { + fprintf(stderr, "Invalid button background color: %s", optarg); + } + break; + case TO_COLOR_DETAILS: /* Details background color */ + if (!parse_color(optarg, &conf->details_background)) { + fprintf(stderr, "Invalid details background color: %s", optarg); + } + break; + case TO_COLOR_TEXT: /* Text color */ + if (!parse_color(optarg, &conf->text)) { + fprintf(stderr, "Invalid text color: %s", optarg); + } + break; + case TO_COLOR_BUTTON_TEXT: /* Button text color */ + if (!parse_color(optarg, &conf->button_text)) { + fprintf(stderr, "Invalid button text color: %s", optarg); + } + break; + case TO_THICK_BAR_BORDER: /* Bottom border thickness */ + conf->bar_border_thickness = strtol(optarg, NULL, 0); + break; + case TO_PADDING_MESSAGE: /* Message padding */ + conf->message_padding = strtol(optarg, NULL, 0); + break; + case TO_THICK_DET_BORDER: /* Details border thickness */ + conf->details_border_thickness = strtol(optarg, NULL, 0); + break; + case TO_THICK_BTN_BORDER: /* Button border thickness */ + conf->button_border_thickness = strtol(optarg, NULL, 0); + break; + case TO_GAP_BTN: /* Gap between buttons */ + conf->button_gap = strtol(optarg, NULL, 0); + break; + case TO_GAP_BTN_DISMISS: /* Gap between dismiss button */ + conf->button_gap_close = strtol(optarg, NULL, 0); + break; + case TO_MARGIN_BTN_RIGHT: /* Margin on the right side of button area */ + conf->button_margin_right = strtol(optarg, NULL, 0); + break; + case TO_PADDING_BTN: /* Padding for the button text */ + conf->button_padding = strtol(optarg, NULL, 0); + break; + default: /* Help or unknown flag */ + fprintf(c == 'h' ? stdout : stderr, "%s", usage); + return LAB_EXIT_FAILURE; + } + } + + return LAB_EXIT_SUCCESS; +} + +int +main(int argc, char **argv) +{ + struct conf conf = { 0 }; + conf_init(&conf); + + struct nag nag = { + .conf = &conf, + }; + + wl_list_init(&nag.buttons); + wl_list_init(&nag.outputs); + wl_list_init(&nag.seats); + + nag.details.details_text = "Toggle details"; + nag.details.close_timeout = 5; + nag.details.use_exclusive_zone = false; + + bool debug = false; + if (argc > 1) { + exit_status = nag_parse_options(argc, argv, &nag, &conf, &debug); + if (exit_status == LAB_EXIT_FAILURE) { + goto cleanup; + } + } + wlr_log_init(debug ? WLR_DEBUG : WLR_ERROR, NULL); + + if (!nag.message) { + wlr_log(WLR_ERROR, "No message passed. Please provide --message/-m"); + exit_status = LAB_EXIT_FAILURE; + goto cleanup; + } + + if (nag.details.message) { + nag.details.button_details = calloc(1, sizeof(struct button)); + assert(nag.details.button_details); + nag.details.button_details->text = nag.details.details_text; + assert(nag.details.button_details->text); + nag.details.button_details->expand = true; + wl_list_insert(nag.buttons.prev, &nag.details.button_details->link); + } + + wlr_log(WLR_DEBUG, "Output: %s", nag.conf->output); + wlr_log(WLR_DEBUG, "Anchors: %lu", (unsigned long)nag.conf->anchors); + wlr_log(WLR_DEBUG, "Message: %s", nag.message); + char *font = pango_font_description_to_string(nag.conf->font_description); + wlr_log(WLR_DEBUG, "Font: %s", font); + free(font); + wlr_log(WLR_DEBUG, "Buttons"); + + struct button *button; + wl_list_for_each(button, &nag.buttons, link) { + wlr_log(WLR_DEBUG, "\t[%s] `%s`", button->text, button->action); + } + + nag_setup(&nag); + + nag_run(&nag); + +cleanup: + nag_destroy(&nag); + return exit_status; +} diff --git a/clients/meson.build b/clients/meson.build new file mode 100644 index 00000000..7c6fa5a5 --- /dev/null +++ b/clients/meson.build @@ -0,0 +1,47 @@ +wayland_client = dependency('wayland-client') +wayland_cursor = dependency('wayland-cursor') + +nag_sources = files( + 'labnag.c', + 'pool-buffer.c', +) + +wl_protocol_dir = wayland_protos.get_variable('pkgdatadir') + +protocols = [ + wl_protocol_dir / 'stable/tablet/tablet-v2.xml', + wl_protocol_dir / 'stable/xdg-shell/xdg-shell.xml', + wl_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml', + '../protocols/wlr-layer-shell-unstable-v1.xml', +] + +foreach xml : protocols + nag_sources += custom_target( + xml.underscorify() + '_c', + input: xml, + output: '@BASENAME@-protocol.c', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + nag_sources += custom_target( + xml.underscorify() + '_client_h', + input: xml, + output: '@BASENAME@-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) +endforeach + +executable( + 'labnag', + nag_sources, + dependencies: [ + cairo, + pangocairo, + glib, + wayland_client, + wayland_cursor, + wlroots, + server_protos, + ], + install: true +) + diff --git a/clients/pool-buffer.c b/clients/pool-buffer.c new file mode 100644 index 00000000..5dabc8aa --- /dev/null +++ b/clients/pool-buffer.c @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Copied from https://github.com/swaywm/sway + * + * Copyright (C) 2016-2017 Drew DeVault + */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "pool-buffer.h" + +static int anonymous_shm_open(void) +{ + int retries = 100; + + do { + // try a probably-unique name + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + pid_t pid = getpid(); + char name[50]; + snprintf(name, sizeof(name), "/labnag-%x-%x", + (unsigned int)pid, (unsigned int)ts.tv_nsec); + + // 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; + } + + --retries; + } while (retries > 0 && errno == EEXIST); + + return -1; +} + +static void buffer_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_release +}; + +static struct pool_buffer *create_buffer(struct wl_shm *shm, + struct pool_buffer *buf, int32_t width, int32_t height, + uint32_t format) +{ + uint32_t stride = width * 4; + size_t size = stride * height; + + int fd = anonymous_shm_open(); + if (fd == -1) { + return NULL; + } + if (ftruncate(fd, size) < 0) { + close(fd); + return NULL; + } + void *data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size); + buf->buffer = wl_shm_pool_create_buffer(pool, 0, + width, height, stride, format); + wl_shm_pool_destroy(pool); + close(fd); + + buf->size = size; + buf->width = width; + buf->height = height; + buf->data = data; + buf->surface = cairo_image_surface_create_for_data(data, + CAIRO_FORMAT_ARGB32, width, height, stride); + buf->cairo = cairo_create(buf->surface); + buf->pango = pango_cairo_create_context(buf->cairo); + + wl_buffer_add_listener(buf->buffer, &buffer_listener, buf); + return buf; +} + +void destroy_buffer(struct pool_buffer *buffer) +{ + if (buffer->buffer) { + wl_buffer_destroy(buffer->buffer); + buffer->buffer = NULL; + } + if (buffer->cairo) { + cairo_destroy(buffer->cairo); + buffer->cairo = NULL; + } + if (buffer->surface) { + cairo_surface_destroy(buffer->surface); + buffer->surface = NULL; + } + if (buffer->pango) { + g_object_unref(buffer->pango); + buffer->pango = NULL; + } + if (buffer->data) { + munmap(buffer->data, buffer->size); + buffer->data = NULL; + } +} + +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) { + destroy_buffer(buffer); + } + + if (!buffer->buffer) { + if (!create_buffer(shm, buffer, width, height, + WL_SHM_FORMAT_ARGB8888)) { + return NULL; + } + } + buffer->busy = true; + return buffer; +} diff --git a/clients/pool-buffer.h b/clients/pool-buffer.h new file mode 100644 index 00000000..28282ca0 --- /dev/null +++ b/clients/pool-buffer.h @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ +/* + * Copied from https://github.com/swaywm/sway + * + * Copyright (C) 2016-2017 Drew DeVault + */ +#ifndef LAB_POOL_BUFFER_H +#define LAB_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 destroy_buffer(struct pool_buffer *buffer); + +#endif /* LAB_POOL_BUFFER_H */ diff --git a/docs/labnag.1.scd b/docs/labnag.1.scd new file mode 100644 index 00000000..60970141 --- /dev/null +++ b/docs/labnag.1.scd @@ -0,0 +1,109 @@ +labnag(1) + +# NAME + +labnag - Show dialog with message and buttons + +# SYNOPSIS + +_labnag_ [options...] + +# OPTIONS + +*-B, --button* [] + Create a button with the text _text_ that optionally executes _action_ + when pressed. Multiple buttons can be defined by providing the flag + multiple times. Buttons will appear in the order they are provided from + lef to right. + +*-Z, --button-dismiss* [] + Create a button with the text _text_ that optionally executes _action_ + when pressed, and dismisses labnag. Multiple buttons can be defined by + providing the flag multiple times. Buttons will appear in the order + they are provided from lef to right. + +*-d, --debug* + Enable debugging. + +*-e, --edge* top|bottom + Set the edge to use. + +*-y, --layer* overlay|top|bottom|background + Set the layer to use. + +*-f, --font* + Set the font to use. + +*-h, --help* + Show help message and quit. + +*-l, --detailed-message* + Read a detailed message from stdin. A button to toggle details will be + added. Details are shown in a scrollable multi-line text area. + +*-L, --detailed-button* + Set the text for the button that toggles details. This has no effect if + there is not a detailed message. The default is _Toggle details_. + +*-m, --message* + Set the message text. + +*-o, --output* + Set the output to use. This should be the name of a _xdg\_output_. + +*-t, --timeout* + Set duration to close dialog. Default is 5 seconds. + +*-x, --exclusive-zone* + Use exclusive zone. Default is false. + +*-v, --version* + Show the version number and quit. + +# APPEARANCE OPTIONS + +*--background* + Set the color of the background. + +*--border* + Set the color of the border. + +*--border-bottom* + Set the color of the bottom border. + +*--button-background* + Set the color for the background for buttons. + +*--text* + Set the text color. + +*--button-text* + Set the button text color. + +*--border-bottom-size* + Set the thickness of the bottom border. + +*--message-padding* + Set the padding for the message. + +*--details-background* + Set the color for the background for details. + +*--details-border-size* + Set the thickness for the details border. + +*--button-border-size* + Set the thickness for the button border. + +*--button-gap* + Set the size of the gap between buttons. + +*--button-dismiss-gap* + Set the size of the gap between the dismiss button and another button. + +*--button-margin-right* + Set the margin from the right of the dismiss button to edge. + +*--button-padding* + Set the padding for the button text. + diff --git a/docs/meson.build b/docs/meson.build index 87acdab1..2f89f2e7 100644 --- a/docs/meson.build +++ b/docs/meson.build @@ -7,6 +7,7 @@ if scdoc.found() 'labwc-config.5', 'labwc-menu.5', 'labwc-theme.5', + 'labnag.1', ] foreach manpage : manpages markdown = manpage + '.scd' diff --git a/meson.build b/meson.build index 3172c7e5..dec66dd3 100644 --- a/meson.build +++ b/meson.build @@ -180,6 +180,7 @@ endif subdir('include') subdir('src') subdir('docs') +subdir('clients') dep_cmocka = dependency('cmocka', required: get_option('test')) if dep_cmocka.found() diff --git a/scripts/check b/scripts/check index 9e108666..40d48b1e 100755 --- a/scripts/check +++ b/scripts/check @@ -19,7 +19,7 @@ run_checks () { return $? fi - find src/ include/ \( -name "*.c" -o -name "*.h" \) -type f -print0 | + find src/ include/ clients/ \( -name "*.c" -o -name "*.h" \) -type f -print0 | nice xargs -0 --max-args 1 --max-procs $(nproc) \ scripts/checkpatch.pl --terse --no-tree --strict --file return $?