diff --git a/include/swaynagbar/nagbar.h b/include/swaynagbar/nagbar.h index 07a0d51e..8b55e4fa 100644 --- a/include/swaynagbar/nagbar.h +++ b/include/swaynagbar/nagbar.h @@ -7,17 +7,26 @@ #define NAGBAR_BAR_BORDER_THICKNESS 2 #define NAGBAR_MESSAGE_PADDING 8 +#define NAGBAR_DETAILS_BORDER_THICKNESS 3 #define NAGBAR_BUTTON_BORDER_THICKNESS 3 #define NAGBAR_BUTTON_GAP 20 #define NAGBAR_BUTTON_GAP_CLOSE 15 #define NAGBAR_BUTTON_MARGIN_RIGHT 2 #define NAGBAR_BUTTON_PADDING 3 +#define NAGBAR_MAX_HEIGHT 500 + enum sway_nagbar_type { NAGBAR_ERROR, NAGBAR_WARNING, }; +enum sway_nagbar_action_type { + NAGBAR_ACTION_DISMISS, + NAGBAR_ACTION_EXPAND, + NAGBAR_ACTION_COMMAND, +}; + struct sway_nagbar_colors { uint32_t button_background; uint32_t background; @@ -43,6 +52,7 @@ struct sway_nagbar_output { struct sway_nagbar_button { char *text; + enum sway_nagbar_action_type type; char *action; int x; int y; @@ -50,6 +60,22 @@ struct sway_nagbar_button { int height; }; +struct sway_nagbar_details { + bool visible; + char *message; + + int x; + int y; + int width; + int height; + + int offset; + int visible_lines; + int total_lines; + struct sway_nagbar_button button_up; + struct sway_nagbar_button button_down; +}; + struct sway_nagbar { bool run_display; int querying_outputs; @@ -77,6 +103,7 @@ struct sway_nagbar { char *message; char *font; list_t *buttons; + struct sway_nagbar_details details; }; void nagbar_setup(struct sway_nagbar *nagbar); diff --git a/swaynagbar/main.c b/swaynagbar/main.c index f5ed064e..389118c6 100644 --- a/swaynagbar/main.c +++ b/swaynagbar/main.c @@ -3,6 +3,7 @@ #include #include "log.h" #include "list.h" +#include "readline.h" #include "swaynagbar/nagbar.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" @@ -34,6 +35,32 @@ static void set_nagbar_colors() { } } +static char *read_from_stdin() { + char *buffer = NULL; + while (!feof(stdin)) { + char *line = read_line(stdin); + if (!line) { + continue; + } + + if (!buffer) { + buffer = strdup(line); + } else { + buffer = realloc(buffer, strlen(buffer) + strlen(line) + 2); + strcat(buffer, line); + strcat(buffer, "\n"); + } + + free(line); + } + + if (buffer && buffer[strlen(buffer) - 1] == '\n') { + buffer[strlen(buffer) - 1] = '\0'; + } + + return buffer; +} + int main(int argc, char **argv) { int exit_code = EXIT_SUCCESS; bool debug = false; @@ -50,17 +77,25 @@ int main(int argc, char **argv) { struct sway_nagbar_button *button_close = calloc(sizeof(struct sway_nagbar_button), 1); button_close->text = strdup("X"); - button_close->action = NULL; + button_close->type = NAGBAR_ACTION_DISMISS; list_add(nagbar.buttons, button_close); - static struct option long_options[] = { + struct sway_nagbar_button *button_details = + calloc(sizeof(struct sway_nagbar_button), 1); + button_details->text = strdup("Toggle Details"); + button_details->type = NAGBAR_ACTION_EXPAND; + + static struct option opts[] = { {"button", required_argument, NULL, 'b'}, {"debug", no_argument, NULL, 'd'}, {"edge", required_argument, NULL, 'e'}, {"font", required_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, + {"detailed-message", required_argument, NULL, 'l'}, + {"detailed-button", required_argument, NULL, 'L'}, {"message", required_argument, NULL, 'm'}, {"output", required_argument, NULL, 'o'}, + {"dismiss-button", required_argument, NULL, 's'}, {"type", required_argument, NULL, 't'}, {"version", no_argument, NULL, 'v'}, {0, 0, 0, 0} @@ -75,26 +110,30 @@ int main(int argc, char **argv) { " -e, --edge top|bottom Set the edge to use.\n" " -f, --font Set the font to use.\n" " -h, --help Show help message and quit.\n" + " -l, --detailed-message Set a detailed message.\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" + " -s, --dismiss-button Set the dismiss button text.\n" " -t, --type error|warning Set the message type.\n" " -v, --version Show the version number and quit.\n"; while (1) { - int c = getopt_long(argc, argv, "b:de:f:hm:o:t:v", long_options, NULL); + int c = getopt_long(argc, argv, "b:de:f:hl:L:m:o:s:t:v", opts, NULL); if (c == -1) { break; } switch (c) { case 'b': // Button if (optind >= argc) { - fprintf(stderr, "Missing action for button %s", optarg); + fprintf(stderr, "Missing action for button %s\n", optarg); exit_code = EXIT_FAILURE; goto cleanup; } struct sway_nagbar_button *button; button = calloc(sizeof(struct sway_nagbar_button), 1); button->text = strdup(optarg); + button->type = NAGBAR_ACTION_COMMAND; button->action = strdup(argv[optind]); optind++; list_add(nagbar.buttons, button); @@ -121,6 +160,20 @@ int main(int argc, char **argv) { free(nagbar.font); nagbar.font = strdup(optarg); break; + case 'l': // Detailed Message + free(nagbar.details.message); + if (strcmp(optarg, "-") == 0) { + nagbar.details.message = read_from_stdin(); + } else { + nagbar.details.message = strdup(optarg); + } + nagbar.details.button_up.text = strdup("▲"); + nagbar.details.button_down.text = strdup("▼"); + break; + case 'L': // Detailed Button Text + free(button_details->text); + button_details->text = strdup(optarg); + break; case 'm': // Message free(nagbar.message); nagbar.message = strdup(optarg); @@ -129,13 +182,17 @@ int main(int argc, char **argv) { free(nagbar.output.name); nagbar.output.name = strdup(optarg); break; + case 's': // Dismiss Button Text + free(button_close->text); + button_close->text = strdup(optarg); + break; case 't': // Type if (strcmp(optarg, "error") == 0) { nagbar.type = NAGBAR_ERROR; } else if (strcmp(optarg, "warning") == 0) { nagbar.type = NAGBAR_WARNING; } else { - fprintf(stderr, "Type must be either 'error' or 'warning'"); + fprintf(stderr, "Type must be either 'error' or 'warning'\n"); exit_code = EXIT_FAILURE; goto cleanup; } @@ -160,6 +217,13 @@ int main(int argc, char **argv) { goto cleanup; } + if (nagbar.details.message) { + list_add(nagbar.buttons, button_details); + } else { + free(button_details->text); + free(button_details); + } + wlr_log(WLR_DEBUG, "Output: %s", nagbar.output.name); wlr_log(WLR_DEBUG, "Anchors: %d", nagbar.anchors); wlr_log(WLR_DEBUG, "Type: %d", nagbar.type); @@ -178,6 +242,8 @@ int main(int argc, char **argv) { return exit_code; cleanup: + free(button_details->text); + free(button_details); nagbar_destroy(&nagbar); return exit_code; } diff --git a/swaynagbar/nagbar.c b/swaynagbar/nagbar.c index 22e5aff4..9e8bbb4b 100644 --- a/swaynagbar/nagbar.c +++ b/swaynagbar/nagbar.c @@ -34,8 +34,11 @@ static bool terminal_execute(char *terminal, char *command) { static void nagbar_button_execute(struct sway_nagbar *nagbar, struct sway_nagbar_button *button) { wlr_log(WLR_DEBUG, "Executing [%s]: %s", button->text, button->action); - if (!button->action) { + if (button->type == NAGBAR_ACTION_DISMISS) { nagbar->run_display = false; + } else if (button->type == NAGBAR_ACTION_EXPAND) { + nagbar->details.visible = !nagbar->details.visible; + render_frame(nagbar); } else { if (fork() == 0) { // Child process. Will be used to prevent zombie processes @@ -119,8 +122,58 @@ static void wl_pointer_button(void *data, struct wl_pointer *wl_pointer, && x < nagbutton->x + nagbutton->width && y < nagbutton->y + nagbutton->height) { nagbar_button_execute(nagbar, nagbutton); + return; } } + + if (nagbar->details.visible && + nagbar->details.total_lines != nagbar->details.visible_lines) { + struct sway_nagbar_button button_up = nagbar->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 + && nagbar->details.offset > 0) { + nagbar->details.offset--; + render_frame(nagbar); + return; + } + + struct sway_nagbar_button button_down = nagbar->details.button_down; + int bot = nagbar->details.total_lines - nagbar->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 + && nagbar->details.offset < bot) { + nagbar->details.offset++; + render_frame(nagbar); + return; + } + } +} + +static void wl_pointer_axis(void *data, struct wl_pointer *wl_pointer, + uint32_t time, uint32_t axis, wl_fixed_t value) { + struct sway_nagbar *nagbar = data; + if (!nagbar->details.visible + || nagbar->pointer.x < nagbar->details.x + || nagbar->pointer.y < nagbar->details.y + || nagbar->pointer.x >= nagbar->details.x + nagbar->details.width + || nagbar->pointer.y >= nagbar->details.y + nagbar->details.height + || nagbar->details.total_lines == nagbar->details.visible_lines) { + return; + } + + int direction = wl_fixed_to_int(value); + int bot = nagbar->details.total_lines - nagbar->details.visible_lines; + if (direction < 0 && nagbar->details.offset > 0) { + nagbar->details.offset--; + } else if (direction > 0 && nagbar->details.offset < bot) { + nagbar->details.offset++; + } + + render_frame(nagbar); } static struct wl_pointer_listener pointer_listener = { @@ -128,7 +181,7 @@ static struct wl_pointer_listener pointer_listener = { .leave = nop, .motion = wl_pointer_motion, .button = wl_pointer_button, - .axis = nop, + .axis = wl_pointer_axis, .frame = nop, .axis_source = nop, .axis_stop = nop, @@ -329,6 +382,9 @@ void nagbar_destroy(struct sway_nagbar *nagbar) { free(button); } list_free(nagbar->buttons); + free(nagbar->details.message); + free(nagbar->details.button_up.text); + free(nagbar->details.button_down.text); if (nagbar->layer_surface) { zwlr_layer_surface_v1_destroy(nagbar->layer_surface); diff --git a/swaynagbar/render.c b/swaynagbar/render.c index c0f59298..7bc2961e 100644 --- a/swaynagbar/render.c +++ b/swaynagbar/render.c @@ -23,11 +23,149 @@ static uint32_t render_message(cairo_t *cairo, struct sway_nagbar *nagbar) { } cairo_set_source_u32(cairo, nagbar->colors.text); - cairo_move_to(cairo, padding, (int)(height / 2.0 - text_height / 2.0)); + cairo_move_to(cairo, padding, (int)(ideal_height - text_height) / 2); pango_printf(cairo, nagbar->font, nagbar->scale, true, "%s", nagbar->message); - return nagbar->height; + return ideal_height; +} + +static void render_details_scroll_button(cairo_t *cairo, + struct sway_nagbar *nagbar, struct sway_nagbar_button *button) { + int text_width, text_height; + get_text_size(cairo, nagbar->font, &text_width, &text_height, + nagbar->scale, true, "%s", button->text); + + int border = NAGBAR_BUTTON_BORDER_THICKNESS * nagbar->scale; + int padding = NAGBAR_BUTTON_PADDING * nagbar->scale; + + cairo_set_source_u32(cairo, nagbar->colors.border); + cairo_rectangle(cairo, button->x, button->y, + button->width, button->height); + cairo_fill(cairo); + + cairo_set_source_u32(cairo, nagbar->colors.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, nagbar->colors.text); + cairo_move_to(cairo, button->x + border + padding, + button->y + border + (button->height - text_height) / 2); + pango_printf(cairo, nagbar->font, nagbar->scale, true, "%s", button->text); +} + +static int get_detailed_scroll_button_width(cairo_t *cairo, + struct sway_nagbar *nagbar) { + int up_width, down_width, temp_height; + get_text_size(cairo, nagbar->font, &up_width, &temp_height, + nagbar->scale, true, "%s", nagbar->details.button_up.text); + get_text_size(cairo, nagbar->font, &down_width, &temp_height, + nagbar->scale, true, "%s", nagbar->details.button_down.text); + + int text_width = up_width > down_width ? up_width : down_width; + int border = NAGBAR_BUTTON_BORDER_THICKNESS * nagbar->scale; + int padding = NAGBAR_BUTTON_PADDING * nagbar->scale; + + return text_width + border * 2 + padding * 2; +} + +static uint32_t render_detailed(cairo_t *cairo, struct sway_nagbar *nagbar, + uint32_t y) { + uint32_t width = nagbar->width * nagbar->scale; + uint32_t height = nagbar->height * nagbar->scale; + height -= NAGBAR_BAR_BORDER_THICKNESS * nagbar->scale; + + int border = NAGBAR_DETAILS_BORDER_THICKNESS * nagbar->scale; + int padding = NAGBAR_MESSAGE_PADDING * nagbar->scale; + int decor = padding + border; + + nagbar->details.x = decor; + nagbar->details.y = y + decor; + nagbar->details.width = width - decor * 2; + + PangoLayout *layout = get_pango_layout(cairo, nagbar->font, + nagbar->details.message, nagbar->scale, true); + pango_layout_set_width(layout, + (nagbar->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); + nagbar->details.total_lines = pango_layout_get_line_count(layout); + + PangoLayoutLine *line; + line = pango_layout_get_line_readonly(layout, nagbar->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 = nagbar->details.offset > 0; + int button_width = get_detailed_scroll_button_width(cairo, nagbar); + if (show_buttons) { + nagbar->details.width -= button_width; + pango_layout_set_width(layout, + (nagbar->details.width - padding * 2) * PANGO_SCALE); + } + + uint32_t ideal_height; + do { + ideal_height = nagbar->details.y + text_height + decor + padding * 2; + if (ideal_height > NAGBAR_MAX_HEIGHT) { + ideal_height = NAGBAR_MAX_HEIGHT; + + if (!show_buttons) { + show_buttons = true; + nagbar->details.width -= button_width; + pango_layout_set_width(layout, + (nagbar->details.width - padding * 2) * PANGO_SCALE); + } + } + + nagbar->details.height = ideal_height - nagbar->details.y - decor; + pango_layout_set_height(layout, + (nagbar->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 != (nagbar->details.height - padding * 2)); + + nagbar->details.visible_lines = pango_layout_get_line_count(layout); + + if (show_buttons) { + nagbar->details.button_up.x = + nagbar->details.x + nagbar->details.width; + nagbar->details.button_up.y = nagbar->details.y; + nagbar->details.button_up.width = button_width; + nagbar->details.button_up.height = nagbar->details.height / 2; + render_details_scroll_button(cairo, nagbar, + &nagbar->details.button_up); + + nagbar->details.button_down.x = + nagbar->details.x + nagbar->details.width; + nagbar->details.button_down.y = + nagbar->details.button_up.y + nagbar->details.button_up.height; + nagbar->details.button_down.width = button_width; + nagbar->details.button_down.height = nagbar->details.height / 2; + render_details_scroll_button(cairo, nagbar, + &nagbar->details.button_down); + } + + cairo_set_source_u32(cairo, nagbar->colors.border); + cairo_rectangle(cairo, nagbar->details.x, nagbar->details.y, + nagbar->details.width, nagbar->details.height); + cairo_fill(cairo); + + cairo_move_to(cairo, nagbar->details.x + padding, + nagbar->details.y + padding); + cairo_set_source_u32(cairo, nagbar->colors.text); + pango_cairo_show_layout(cairo, layout); + g_object_unref(layout); + + return ideal_height; } static uint32_t render_button(cairo_t *cairo, struct sway_nagbar *nagbar, @@ -50,7 +188,7 @@ static uint32_t render_button(cairo_t *cairo, struct sway_nagbar *nagbar, } button->x = *x - border - text_width - padding * 2; - button->y = (int)(height / 2.0 - text_height / 2.0) - padding; + button->y = (int)(ideal_height - text_height) / 2 - padding; button->width = text_width + padding * 2; button->height = text_height + padding * 2; @@ -70,7 +208,7 @@ static uint32_t render_button(cairo_t *cairo, struct sway_nagbar *nagbar, *x = button->x - border; - return nagbar->height; + return ideal_height; } static uint32_t render_to_cairo(cairo_t *cairo, struct sway_nagbar *nagbar) { @@ -93,6 +231,11 @@ static uint32_t render_to_cairo(cairo_t *cairo, struct sway_nagbar *nagbar) { } } + if (nagbar->details.visible) { + h = render_detailed(cairo, nagbar, max_height); + max_height = h > max_height ? h : max_height; + } + int border = NAGBAR_BAR_BORDER_THICKNESS * nagbar->scale; if (max_height > nagbar->height) { max_height += border; @@ -102,7 +245,7 @@ static uint32_t render_to_cairo(cairo_t *cairo, struct sway_nagbar *nagbar) { nagbar->width * nagbar->scale, border); cairo_fill(cairo); - return max_height > nagbar->height ? max_height : nagbar->height; + return max_height; } void render_frame(struct sway_nagbar *nagbar) { @@ -129,13 +272,16 @@ void render_frame(struct sway_nagbar *nagbar) { nagbar->buffers, nagbar->width * nagbar->scale, nagbar->height * nagbar->scale); - cairo_t *shm = nagbar->current_buffer->cairo; + if (!nagbar->current_buffer) { + wlr_log(WLR_DEBUG, "Failed to get buffer. Skipping frame."); + return; + } + cairo_t *shm = nagbar->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); diff --git a/swaynagbar/swaynagbar.1.scd b/swaynagbar/swaynagbar.1.scd index 4235e2ea..2a34ae68 100644 --- a/swaynagbar/swaynagbar.1.scd +++ b/swaynagbar/swaynagbar.1.scd @@ -25,6 +25,15 @@ _swaynagbar_ [options...] *-h, --help* Show help message and quit. +*-l, --detailed-message * + Set the detailed message. A button to toggle details will be added. Details + are shown in a scrollable multi-line text area. If _msg_ is _-_, then the + detailed message will be read from stdin. + +*-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. @@ -33,6 +42,9 @@ _swaynagbar_ [options...] _xdg\_output\_manager_ is not supported, then the first detected output will be used +*-s, --dismiss-button * + Sets the text for the dismiss nagbar button. The default is _X_. + *-t, --type error|warning* Set the message type.