diff --git a/README.md b/README.md index c4c7c63..62a34a7 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Dependencies: - scdoc (optional) ``` -$ meson build +$ meson setup build $ ninja -C build # ninja -C build install ``` @@ -30,14 +30,3 @@ To use wmenu with Sway, you can add the following to your configuration file: set $menu wmenu-run bindsym $mod+d exec $menu ``` - -## Contributing - -Send patches and questions to [~adnano/wmenu-devel](https://lists.sr.ht/~adnano/wmenu-devel). - -Subscribe to release announcements on [~adnano/wmenu-announce](https://lists.sr.ht/~adnano/wmenu-announce). - -## Credits - -This project started as a fork of [dmenu-wl](https://github.com/nyyManni/dmenu-wayland). -However, most of the code was rewritten from scratch. diff --git a/docs/wmenu.1.scd b/docs/wmenu.1.scd index 1ee623a..4519e8b 100644 --- a/docs/wmenu.1.scd +++ b/docs/wmenu.1.scd @@ -6,7 +6,7 @@ wmenu - dynamic menu for Wayland # SYNOPSIS -*wmenu* [-biv] \ +*wmenu* [-biPv] \ [-f _font_] \ [-l _lines_] \ [-o _output_] \ @@ -35,6 +35,10 @@ $PATH and runs the result. *-i* wmenu matches menu items case insensitively. +*-P* + wmenu will not directly display the keyboard input, but instead replace it + with asterisks. + *-v* prints version information to stdout, then exits. diff --git a/menu.c b/menu.c index 3e8f9be..207d71c 100644 --- a/menu.c +++ b/menu.c @@ -1,4 +1,5 @@ #define _POSIX_C_SOURCE 200809L +#include #include #include #include @@ -22,7 +23,7 @@ #include "wayland.h" // Creates and returns a new menu. -struct menu *menu_create() { +struct menu *menu_create(menu_callback callback) { struct menu *menu = calloc(1, sizeof(struct menu)); menu->strncmp = strncmp; menu->font = "monospace 10"; @@ -32,6 +33,9 @@ struct menu *menu_create() { menu->promptfg = 0xeeeeeeff; menu->selectionbg = 0x005577ff; menu->selectionfg = 0xeeeeeeff; + menu->callback = callback; + menu->test_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 1, 1); + menu->test_cairo = cairo_create(menu->test_surface); return menu; } @@ -44,24 +48,20 @@ static void free_pages(struct menu *menu) { } } -static void free_item(struct item *item) { - free(item->text); - free(item); -} - static void free_items(struct menu *menu) { - struct item *next = menu->items; - while (next) { - struct item *item = next; - next = item->next; - free_item(item); + for (size_t i = 0; i < menu->item_count; i++) { + struct item *item = &menu->items[i]; + free(item->text); } + free(menu->items); } // Destroys the menu, freeing memory associated with it. void menu_destroy(struct menu *menu) { free_pages(menu); free_items(menu); + cairo_destroy(menu->test_cairo); + cairo_surface_destroy(menu->test_surface); free(menu); } @@ -85,11 +85,11 @@ static bool parse_color(const char *color, uint32_t *result) { // Parse menu options from command line arguments. void menu_getopts(struct menu *menu, int argc, char *argv[]) { const char *usage = - "Usage: wmenu [-biPvx] [-f font] [-l lines] [-o output] [-p prompt]\n" + "Usage: wmenu [-biPv] [-f font] [-l lines] [-o output] [-p prompt]\n" "\t[-N color] [-n color] [-M color] [-m color] [-S color] [-s color]\n"; int opt; - while ((opt = getopt(argc, argv, "bhivf:l:o:p:N:n:M:m:S:s:")) != -1) { + while ((opt = getopt(argc, argv, "bhiPvf:l:o:p:N:n:M:m:S:s:")) != -1) { switch (opt) { case 'b': menu->bottom = true; @@ -97,6 +97,9 @@ void menu_getopts(struct menu *menu, int argc, char *argv[]) { case 'i': menu->strncmp = strncasecmp; break; + case 'P': + menu->passwd = true; + break; case 'v': puts("wmenu " VERSION); exit(EXIT_SUCCESS); @@ -163,34 +166,44 @@ void menu_getopts(struct menu *menu, int argc, char *argv[]) { } // Add an item to the menu. -void menu_add_item(struct menu *menu, char *text, bool sort) { - struct item *new = calloc(1, sizeof(struct item)); - if (!new) { - return; +void menu_add_item(struct menu *menu, char *text) { + if ((menu->item_count & (menu->item_count - 1)) == 0) { + size_t alloc_size = menu->item_count ? 2 * menu->item_count : 1; + void *new_array = realloc(menu->items, sizeof(struct item) * alloc_size); + if (!new_array) { + fprintf(stderr, "could not realloc %zu bytes", sizeof(struct item) * alloc_size); + exit(EXIT_FAILURE); + } + menu->items = new_array; } + + struct item *new = &menu->items[menu->item_count]; new->text = text; - if (sort) { - for (struct item **item = &menu->items; *item; item = &(*item)->next) { - int result = strcmp(new->text, (*item)->text); - if (result == 0) { - free_item(new); - return; - } - if (result < 0) { - new->next = *item; - *item = new; - return; - } + menu->item_count++; +} + +static int compare_items(const void *a, const void *b) { + const struct item *item_a = a; + const struct item *item_b = b; + return strcmp(item_a->text, item_b->text); +} + +void menu_sort_and_deduplicate(struct menu *menu) { + size_t j = 1; + size_t i; + + qsort(menu->items, menu->item_count, sizeof(*menu->items), compare_items); + + for (i = 1; i < menu->item_count; i++) { + if (strcmp(menu->items[i].text, menu->items[j - 1].text) == 0) { + free(menu->items[i].text); + } else { + menu->items[j] = menu->items[i]; + j++; } } - - if (menu->lastitem) { - menu->lastitem->next = new; - } else { - menu->items = new; - } - menu->lastitem = new; + menu->item_count = j; } static void append_page(struct page *page, struct page **first, struct page **last) { @@ -287,6 +300,7 @@ static void match_items(struct menu *menu) { char buf[sizeof menu->input], *tok; char **tokv = NULL; int i, tokc = 0; + size_t k; size_t tok_len; menu->matches = NULL; menu->matches_end = NULL; @@ -310,8 +324,8 @@ static void match_items(struct menu *menu) { } tok_len = tokc ? strlen(tokv[0]) : 0; - struct item *item; - for (item = menu->items; item; item = item->next) { + for (k = 0; k < menu->item_count; k++) { + struct item *item = &menu->items[k]; for (i = 0; i < tokc; i++) { if (!fstrstr(menu, item->text, tokv[i])) { /* token does not match */ @@ -362,9 +376,13 @@ static void match_items(struct menu *menu) { } } +// Marks the menu as needing to be rendered again. +void menu_invalidate(struct menu *menu) { + menu->rendered = false; +} + // Render menu items. void menu_render_items(struct menu *menu) { - render_menu(menu); calc_widths(menu); match_items(menu); render_menu(menu); @@ -485,13 +503,13 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, // Delete right menu->input[menu->cursor] = '\0'; match_items(menu); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_u: // Delete left insert(menu, NULL, 0 - menu->cursor); match_items(menu); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_w: // Delete word @@ -502,7 +520,7 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, insert(menu, NULL, nextrune(menu, -1) - menu->cursor); } match_items(menu); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_Y: // Paste clipboard @@ -510,17 +528,17 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, return; } match_items(menu); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_Left: case XKB_KEY_KP_Left: movewordedge(menu, -1); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_Right: case XKB_KEY_KP_Right: movewordedge(menu, +1); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_Return: @@ -534,11 +552,11 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, switch (sym) { case XKB_KEY_b: movewordedge(menu, -1); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_f: movewordedge(menu, +1); - render_menu(menu); + menu_invalidate(menu); return; case XKB_KEY_g: sym = XKB_KEY_Home; @@ -568,18 +586,10 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, case XKB_KEY_Return: case XKB_KEY_KP_Enter: if (shift) { - puts(menu->input); - fflush(stdout); - menu->exit = true; - } else if (menu->callback) { - menu->callback(menu); + menu->callback(menu, menu->input, true); } else { char *text = menu->sel ? menu->sel->text : menu->input; - puts(text); - fflush(stdout); - if (!ctrl) { - menu->exit = true; - } + menu->callback(menu, text, !ctrl); } break; case XKB_KEY_Left: @@ -588,10 +598,10 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, case XKB_KEY_KP_Up: if (menu->sel && menu->sel->prev_match) { menu->sel = menu->sel->prev_match; - render_menu(menu); + menu_invalidate(menu); } else if (menu->cursor > 0) { menu->cursor = nextrune(menu, -1); - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_Right: @@ -600,51 +610,51 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, case XKB_KEY_KP_Down: if (menu->cursor < len) { menu->cursor = nextrune(menu, +1); - render_menu(menu); + menu_invalidate(menu); } else if (menu->sel && menu->sel->next_match) { menu->sel = menu->sel->next_match; - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_Prior: case XKB_KEY_KP_Prior: if (menu->sel && menu->sel->page->prev) { menu->sel = menu->sel->page->prev->first; - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_Next: case XKB_KEY_KP_Next: if (menu->sel && menu->sel->page->next) { menu->sel = menu->sel->page->next->first; - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_Home: case XKB_KEY_KP_Home: if (menu->sel == menu->matches) { menu->cursor = 0; - render_menu(menu); + menu_invalidate(menu); } else { menu->sel = menu->matches; - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_End: case XKB_KEY_KP_End: if (menu->cursor < len) { menu->cursor = len; - render_menu(menu); + menu_invalidate(menu); } else { menu->sel = menu->matches_end; - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_BackSpace: if (menu->cursor > 0) { insert(menu, NULL, nextrune(menu, -1) - menu->cursor); match_items(menu); - render_menu(menu); + menu_invalidate(menu); } break; case XKB_KEY_Delete: @@ -655,7 +665,7 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, menu->cursor = nextrune(menu, +1); insert(menu, NULL, nextrune(menu, -1) - menu->cursor); match_items(menu); - render_menu(menu); + menu_invalidate(menu); break; case XKB_KEY_Tab: if (!menu->sel) { @@ -665,7 +675,7 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, memcpy(menu->input, menu->sel->text, menu->cursor); menu->input[menu->cursor] = '\0'; match_items(menu); - render_menu(menu); + menu_invalidate(menu); break; case XKB_KEY_Escape: menu->exit = true; @@ -675,7 +685,7 @@ void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, if (xkb_keysym_to_utf8(sym, buf, 8)) { insert(menu, buf, strnlen(buf, 8)); match_items(menu); - render_menu(menu); + menu_invalidate(menu); } } } diff --git a/menu.h b/menu.h index 0a6b22c..fdd9ad2 100644 --- a/menu.h +++ b/menu.h @@ -1,16 +1,19 @@ #ifndef WMENU_MENU_H #define WMENU_MENU_H +#include #include #include #include #include +struct menu; +typedef void (*menu_callback)(struct menu *menu, char *text, bool exit); + // A menu item. struct item { char *text; int width; - struct item *next; // traverses all items struct item *prev_match; // previous matching item struct item *next_match; // next matching item struct page *page; // the page holding this item @@ -30,6 +33,8 @@ struct menu { bool bottom; // The function used to match menu items int (*strncmp)(const char *, const char *, size_t); + // Whether the input is a password + bool passwd; // The font used to display the menu char *font; // The number of lines to list items vertically @@ -47,6 +52,10 @@ struct menu { struct wl_context *context; + // 1x1 surface used estimate text sizes with pango + cairo_surface_t *test_surface; + cairo_t *test_cairo; + int width; int height; int line_height; @@ -55,26 +64,29 @@ struct menu { int promptw; int left_arrow; int right_arrow; + bool rendered; char input[BUFSIZ]; size_t cursor; - struct item *items; // list of all items - struct item *lastitem; // last item in the list + struct item *items; // array of all items + size_t item_count; struct item *matches; // list of matching items struct item *matches_end; // last matching item struct item *sel; // selected item struct page *pages; // list of pages - void (*callback)(struct menu *menu); + menu_callback callback; bool exit; bool failure; }; -struct menu *menu_create(); +struct menu *menu_create(menu_callback callback); void menu_destroy(struct menu *menu); void menu_getopts(struct menu *menu, int argc, char *argv[]); -void menu_add_item(struct menu *menu, char *text, bool sort); +void menu_add_item(struct menu *menu, char *text); +void menu_sort_and_deduplicate(struct menu *menu); +void menu_invalidate(struct menu *menu); void menu_render_items(struct menu *menu); void menu_paste(struct menu *menu, const char *text, ssize_t len); void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, diff --git a/meson.build b/meson.build index d2643fc..3a5cb18 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'wmenu', 'c', - version: '0.1.8', + version: '0.2.0', license: 'MIT', default_options: [ 'c_std=c11', diff --git a/render.c b/render.c index e33898d..e86d93b 100644 --- a/render.c +++ b/render.c @@ -13,8 +13,13 @@ // Calculate text widths. void calc_widths(struct menu *menu) { struct wl_context *context = menu->context; - struct pool_buffer *current = context_get_current_buffer(context); - cairo_t *cairo = current->cairo; + int scale = context_get_scale(context); + cairo_surface_set_device_scale(menu->test_surface, scale, scale); + cairo_set_antialias(menu->test_cairo, CAIRO_ANTIALIAS_BEST); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_set_font_options(menu->test_cairo, fo); + cairo_font_options_destroy(fo); + cairo_t *cairo = menu->test_cairo; // Calculate prompt width if (menu->prompt) { @@ -28,7 +33,8 @@ void calc_widths(struct menu *menu) { menu->right_arrow = text_width(cairo, menu->font, ">") + 2 * menu->padding; // Calculate item widths and input area width - for (struct item *item = menu->items; item; item = item->next) { + for (size_t i = 0; i < menu->item_count; i++) { + struct item *item = &menu->items[i]; item->width = text_width(cairo, menu->font, item->text); if (item->width > menu->inputw) { menu->inputw = item->width; @@ -79,8 +85,22 @@ static void render_prompt(struct menu *menu, cairo_t *cairo) { // Renders the input text. static void render_input(struct menu *menu, cairo_t *cairo) { - render_text(menu, cairo, menu->input, menu->promptw, 0, 0, - 0, menu->normalfg, menu->padding, menu->padding); + char *censort = NULL; + + if (menu->passwd) { + censort = calloc(1, sizeof(menu->input)); + if (!censort) { + return; + } + memset(censort, '*', strlen(menu->input)); + } + + render_text(menu, cairo, menu->passwd ? censort : menu->input, + menu->promptw, 0, 0, 0, menu->normalfg, menu->padding, menu->padding); + + if (censort) { + free(censort); + } } // Renders a cursor for the input field. @@ -169,33 +189,18 @@ static void render_to_cairo(struct menu *menu, cairo_t *cairo) { void render_menu(struct menu *menu) { struct wl_context *context = menu->context; - cairo_surface_t *recorder = cairo_recording_surface_create( - CAIRO_CONTENT_COLOR_ALPHA, NULL); - cairo_t *cairo = cairo_create(recorder); - cairo_set_antialias(cairo, CAIRO_ANTIALIAS_BEST); - cairo_font_options_t *fo = cairo_font_options_create(); - cairo_set_font_options(cairo, fo); - cairo_font_options_destroy(fo); - cairo_save(cairo); - cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); - cairo_paint(cairo); - cairo_restore(cairo); - - render_to_cairo(menu, cairo); - int scale = context_get_scale(context); struct pool_buffer *buffer = context_get_next_buffer(context, scale); if (!buffer) { - goto cleanup; + return; } cairo_t *shm = 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); - cairo_paint(shm); + cairo_set_antialias(shm, CAIRO_ANTIALIAS_BEST); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_set_font_options(shm, fo); + cairo_font_options_destroy(fo); + render_to_cairo(menu, shm); struct wl_surface *surface = context_get_surface(context); wl_surface_set_buffer_scale(surface, scale); @@ -203,7 +208,5 @@ void render_menu(struct menu *menu) { wl_surface_damage(surface, 0, 0, menu->width, menu->height); wl_surface_commit(surface); -cleanup: - cairo_destroy(cairo); - cairo_surface_destroy(recorder); + menu->rendered = true; } diff --git a/wayland.c b/wayland.c index d31ca20..823ced1 100644 --- a/wayland.c +++ b/wayland.c @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -18,6 +17,7 @@ #include "menu.h" #include "pool-buffer.h" +#include "render.h" #include "wayland.h" #include "xdg-activation-v1-client-protocol.h" #include "wlr-layer-shell-unstable-v1-client-protocol.h" @@ -208,6 +208,7 @@ static void noop() { static void surface_enter(void *data, struct wl_surface *surface, struct wl_output *wl_output) { struct wl_context *context = data; context->output = wl_output_get_user_data(wl_output); + menu_invalidate(context->menu); } static const struct wl_surface_listener surface_listener = { @@ -439,7 +440,7 @@ int menu_run(struct menu *menu) { context->layer_shell, context->surface, context->output ? context->output->output : NULL, - ZWLR_LAYER_SHELL_V1_LAYER_TOP, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "menu" ); assert(layer_surface != NULL); @@ -492,13 +493,17 @@ int menu_run(struct menu *menu) { if (fds[1].revents & POLLIN) { keyboard_repeat(context->keyboard); } + + // Render the menu if necessary + if (!menu->rendered) { + render_menu(menu); + } } - bool failure = menu->failure; context_destroy(context); menu->context = NULL; - if (failure) { + if (menu->failure) { return EXIT_FAILURE; } return EXIT_SUCCESS; diff --git a/wmenu-run.c b/wmenu-run.c index 228edc4..1b7b8c1 100644 --- a/wmenu-run.c +++ b/wmenu-run.c @@ -1,6 +1,6 @@ #define _POSIX_C_SOURCE 200809L #include -#include +#include #include #include #include @@ -20,56 +20,56 @@ static void read_items(struct menu *menu) { if (ent->d_name[0] == '.') { continue; } - menu_add_item(menu, strdup(ent->d_name), true); + menu_add_item(menu, strdup(ent->d_name)); } closedir(dir); } + menu_sort_and_deduplicate(menu); free(path); } -struct executable { +struct command { struct menu *menu; - char *name; + char *text; + bool exit; }; static void activation_token_done(void *data, struct xdg_activation_token_v1 *activation_token, const char *token) { - struct executable *exe = data; + struct command *cmd = data; xdg_activation_token_v1_destroy(activation_token); - menu_destroy(exe->menu); - setenv("XDG_ACTIVATION_TOKEN", token, true); - execlp(exe->name, exe->name, NULL); - - fprintf(stderr, "Failed to execute selection: %s\n", strerror(errno)); - free(exe->name); - free(exe); - exit(EXIT_FAILURE); + int pid = fork(); + if (pid == 0) { + setenv("XDG_ACTIVATION_TOKEN", token, true); + char *argv[] = {"/bin/sh", "-c", cmd->text, NULL}; + execvp(argv[0], (char**)argv); + } else { + if (cmd->exit) { + cmd->menu->exit = true; + } + } } static const struct xdg_activation_token_v1_listener activation_token_listener = { .done = activation_token_done, }; -static void exec(struct menu *menu) { - if (!menu->sel) { - return; - } - - struct executable *exe = calloc(1, sizeof(struct executable)); - exe->menu = menu; - exe->name = strdup(menu->sel->text); +static void exec_item(struct menu *menu, char *text, bool exit) { + struct command *cmd = calloc(1, sizeof(struct command)); + cmd->menu = menu; + cmd->text = strdup(text); + cmd->exit = exit; struct xdg_activation_v1 *activation = context_get_xdg_activation(menu->context); struct xdg_activation_token_v1 *activation_token = xdg_activation_v1_get_activation_token(activation); xdg_activation_token_v1_set_surface(activation_token, context_get_surface(menu->context)); - xdg_activation_token_v1_add_listener(activation_token, &activation_token_listener, exe); + xdg_activation_token_v1_add_listener(activation_token, &activation_token_listener, cmd); xdg_activation_token_v1_commit(activation_token); } int main(int argc, char *argv[]) { - struct menu *menu = menu_create(); - menu->callback = exec; + struct menu *menu = menu_create(exec_item); menu_getopts(menu, argc, argv); read_items(menu); int status = menu_run(menu); diff --git a/wmenu.c b/wmenu.c index e7d37d9..38e78b9 100644 --- a/wmenu.c +++ b/wmenu.c @@ -1,5 +1,6 @@ #define _POSIX_C_SOURCE 200809L +#include #include #include "menu.h" @@ -12,12 +13,20 @@ static void read_items(struct menu *menu) { if (p) { *p = '\0'; } - menu_add_item(menu, strdup(buf), false); + menu_add_item(menu, strdup(buf)); + } +} + +static void print_item(struct menu *menu, char *text, bool exit) { + puts(text); + fflush(stdout); + if (exit) { + menu->exit = true; } } int main(int argc, char *argv[]) { - struct menu *menu = menu_create(); + struct menu *menu = menu_create(print_item); menu_getopts(menu, argc, argv); read_items(menu); int status = menu_run(menu);