wmenu

fork of wmenu with my settings
git clone https://git.awy.one/wmenu.git
Log | Files | Refs | README | LICENSE

commit 8e2c3ba8600513ea110a81903c428f24c5937991
Author: awy <awy@awy.one>
Date:   Thu, 20 Mar 2025 16:58:05 +0300

upload

Diffstat:
A.gitignore | 1+
ALICENSE | 32++++++++++++++++++++++++++++++++
AREADME.md | 32++++++++++++++++++++++++++++++++
Adocs/meson.build | 26++++++++++++++++++++++++++
Adocs/wmenu.1.scd | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amenu.c | 691+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Amenu.h | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ameson.build | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apango.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apango.h | 16++++++++++++++++
Apool-buffer.c | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apool-buffer.h | 26++++++++++++++++++++++++++
Aprotocols/meson.build | 47+++++++++++++++++++++++++++++++++++++++++++++++
Aprotocols/wlr-layer-shell-unstable-v1.xml | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arender.c | 212+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Arender.h | 9+++++++++
Awayland.c | 510+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awayland.h | 19+++++++++++++++++++
Awmenu-run.c | 78++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awmenu.c | 35+++++++++++++++++++++++++++++++++++
20 files changed, 2597 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +build diff --git a/LICENSE b/LICENSE @@ -0,0 +1,32 @@ +MIT/X Consortium License + +© 2006-2019 Anselm R Garbe <anselm@garbe.ca> +© 2006-2008 Sander van Dijk <a.h.vandijk@gmail.com> +© 2006-2007 Michał Janeczek <janeczek@gmail.com> +© 2007 Kris Maglione <jg@suckless.org> +© 2009 Gottox <gottox@s01.de> +© 2009 Markus Schnalke <meillo@marmaro.de> +© 2009 Evan Gates <evan.gates@gmail.com> +© 2010-2012 Connor Lane Smith <cls@lubutu.com> +© 2014-2020 Hiltjo Posthuma <hiltjo@codemadness.org> +© 2015-2019 Quentin Rameau <quinq@fifth.space> +© 2018-2019 Henrik Nyman <h@nyymanni.com> +© 2022 adnano + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,32 @@ +# wmenu + +wmenu is an efficient dynamic menu for Sway and wlroots based Wayland +compositors. It provides a Wayland-native dmenu replacement which maintains the +look and feel of dmenu. + +## Installation + +Dependencies: + +- cairo +- pango +- wayland +- xkbcommon +- scdoc (optional) + +``` +$ meson setup build +$ ninja -C build +# ninja -C build install +``` + +## Usage + +See wmenu(1) + +To use wmenu with Sway, you can add the following to your configuration file: + +``` +set $menu wmenu-run +bindsym $mod+d exec $menu +``` diff --git a/docs/meson.build b/docs/meson.build @@ -0,0 +1,26 @@ +scdoc_dep = dependency('scdoc', version: '>=1.9.2', native: true, required: false) + +if scdoc_dep.found() + scdoc = find_program( + scdoc_dep.get_pkgconfig_variable('scdoc'), + native: true, + ) + mandir = get_option('mandir') + + docs = [ + 'wmenu.1', + ] + + foreach path : docs + custom_target( + path, + output: path, + input: '@0@.scd'.format(path), + capture: true, + feed: true, + command: [scdoc], + install: true, + install_dir: '@0@/man1'.format(mandir) + ) + endforeach +endif diff --git a/docs/wmenu.1.scd b/docs/wmenu.1.scd @@ -0,0 +1,186 @@ +WMENU(1) + +# NAME + +wmenu - dynamic menu for Wayland + +# SYNOPSIS + +*wmenu* [-biPv] \ + [-f _font_] \ + [-l _lines_] \ + [-o _output_] \ + [-p _prompt_] \ + [-N _color_] [-n _color_] \ + [-M _color_] [-m _color_] \ + [-S _color_] [-s _color_] + +*wmenu-run* ... + +# DESCRIPTION + +*wmenu* is a dynamic menu for Wayland, which reads a list of newline-separated +items from stdin. When the user selects an item and presses Return, their choice +is printed to stdout and wmenu terminates. Entering text will narrow the items +to those matching the tokens in the input. + +*wmenu-run* is a special invocation of wmenu which lists programs in the user's +$PATH and runs the result. + +# OPTIONS + +*-b* + wmenu appears at the bottom of the screen. + +*-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. + +*-f* _font_ + defines the font used. For more information, see + https://docs.gtk.org/Pango/type_func.FontDescription.from_string.html + +*-l* _lines_ + wmenu lists items vertically, with the given number of lines. + +*-o* _output_ + wmenu is displayed on the output with the given name. + +*-p* _prompt_ + defines the prompt to be displayed to the left of the input field. + +*-N* _RRGGBB[AA]_ + defines the normal background color. + +*-n* _RRGGBB[AA]_ + defines the normal foreground color. + +*-M* _RRGGBB[AA]_ + defines the prompt background color. + +*-m* _RRGGBB[AA]_ + defines the prompt foreground color. + +*-S* _RRGGBB[AA]_ + defines the selection background color. + +*-s* _RRGGBB[AA]_ + defines the selection foreground color. + +# USAGE + +wmenu is completely controlled by the keyboard. Items are selected using the +arrow keys, page up, page down, home, and end. + +*Tab* + Copy the selected item to the input field. + +*Return* + Confirm selection. Prints the selected item to stdout and exits, returning + success. + +*Ctrl-Return* + Confirm selection. Prints the selected item to stdout and continues. + +*Shift-Return* + Confirm input. Prints the input text to stdout and exits, returning success. + +*Escape* + Exit without selecting an item, returning failure. + +*Ctrl-Left* + Move cursor to the start of the current word. + +*Ctrl-Right* + Move cursor to the end of the current word. + +|[ *C-a* +:< Home + +|[ *C-b* +:< Left + +|[ *C-c* +:< Escape + +|[ *C-d* +:< Delete + +|[ *C-e* +:< End + +|[ *C-f* +:< Right + +|[ *C-g* +:< Escape + +|[ *C-[* +:< Escape + +|[ *C-h* +:< Backspace + +|[ *C-i* +:< Tab + +|[ *C-j* +:< Return + +|[ *C-J* +:< Shift-Return + +|[ *C-k* +:< Delete line right + +|[ *C-m* +:< Return + +|[ *C-M* +:< Shift-Return + +|[ *C-n* +:< Down + +|[ *C-p* +:< Up + +|[ *C-u* +:< Delete line left + +|[ *C-w* +:< Delete word left + +|[ *C-Y* +:< Paste from Wayland clipboard + +|[ *M-b* +:< Move cursor to the start of the current word + +|[ *M-f* +:< Move cursor to the end of the current word + +|[ *M-g* +:< Home + +|[ *M-G* +:< End + +|[ *M-h* +:< Up + +|[ *M-j* +:< Page down + +|[ *M-k* +:< Page up + +|[ *M-l* +:< Down + diff --git a/menu.c b/menu.c @@ -0,0 +1,691 @@ +#define _POSIX_C_SOURCE 200809L +#include <assert.h> +#include <ctype.h> +#include <poll.h> +#include <stdbool.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/timerfd.h> +#include <wayland-client.h> +#include <wayland-client-protocol.h> +#include <xkbcommon/xkbcommon.h> + +#include "menu.h" + +#include "pango.h" +#include "render.h" +#include "wayland.h" + +// Creates and returns a new menu. +struct menu *menu_create(menu_callback callback) { + struct menu *menu = calloc(1, sizeof(struct menu)); + menu->strncmp = strncmp; + menu->font = "monospace 12"; + menu->normalbg = 0x2e3440ff; + menu->normalfg = 0xd8dee9ff; + menu->promptbg = 0xeacb8aff; + menu->promptfg = 0x3b4252ff; + menu->selectionbg = 0xeacb8aff; + menu->selectionfg = 0x3b4252ff; + 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; +} + +static void free_pages(struct menu *menu) { + struct page *next = menu->pages; + while (next) { + struct page *page = next; + next = page->next; + free(page); + } +} + +static void free_items(struct menu *menu) { + 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); +} + +static bool parse_color(const char *color, uint32_t *result) { + if (color[0] == '#') { + ++color; + } + size_t len = strlen(color); + if ((len != 6 && len != 8) || !isxdigit(color[0]) || !isxdigit(color[1])) { + return false; + } + char *ptr; + uint32_t parsed = (uint32_t)strtoul(color, &ptr, 16); + if (*ptr != '\0') { + return false; + } + *result = len == 6 ? ((parsed << 8) | 0xFF) : parsed; + return true; +} + +// Parse menu options from command line arguments. +void menu_getopts(struct menu *menu, int argc, char *argv[]) { + const char *usage = + "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, "bhiPvf:l:o:p:N:n:M:m:S:s:")) != -1) { + switch (opt) { + case 'b': + menu->bottom = true; + break; + case 'i': + menu->strncmp = strncasecmp; + break; + case 'P': + menu->passwd = true; + break; + case 'v': + puts("wmenu " VERSION); + exit(EXIT_SUCCESS); + case 'f': + menu->font = optarg; + break; + case 'l': + menu->lines = atoi(optarg); + break; + case 'o': + menu->output_name = optarg; + break; + case 'p': + menu->prompt = optarg; + break; + case 'N': + if (!parse_color(optarg, &menu->normalbg)) { + fprintf(stderr, "Invalid background color: %s", optarg); + } + break; + case 'n': + if (!parse_color(optarg, &menu->normalfg)) { + fprintf(stderr, "Invalid foreground color: %s", optarg); + } + break; + case 'M': + if (!parse_color(optarg, &menu->promptbg)) { + fprintf(stderr, "Invalid prompt background color: %s", optarg); + } + break; + case 'm': + if (!parse_color(optarg, &menu->promptfg)) { + fprintf(stderr, "Invalid prompt foreground color: %s", optarg); + } + break; + case 'S': + if (!parse_color(optarg, &menu->selectionbg)) { + fprintf(stderr, "Invalid selection background color: %s", optarg); + } + break; + case 's': + if (!parse_color(optarg, &menu->selectionfg)) { + fprintf(stderr, "Invalid selection foreground color: %s", optarg); + } + break; + default: + fprintf(stderr, "%s", usage); + exit(EXIT_FAILURE); + } + } + + if (optind < argc) { + fprintf(stderr, "%s", usage); + exit(EXIT_FAILURE); + } + + int height = get_font_height(menu->font); + menu->line_height = height + 2; + menu->height = menu->line_height; + if (menu->lines > 0) { + menu->height += menu->height * menu->lines; + } + menu->padding = height / 2; +} + +// Add an item to the menu. +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; + + 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++; + } + } + menu->item_count = j; +} + +static void append_page(struct page *page, struct page **first, struct page **last) { + if (*last) { + (*last)->next = page; + } else { + *first = page; + } + page->prev = *last; + page->next = NULL; + *last = page; +} + +static void page_items(struct menu *menu) { + // Free existing pages + while (menu->pages != NULL) { + struct page *page = menu->pages; + menu->pages = menu->pages->next; + free(page); + } + + if (!menu->matches) { + return; + } + + // Make new pages + if (menu->lines > 0) { + struct page *pages_end = NULL; + struct item *item = menu->matches; + while (item) { + struct page *page = calloc(1, sizeof(struct page)); + page->first = item; + + for (int i = 1; item && i <= menu->lines; i++) { + item->page = page; + page->last = item; + item = item->next_match; + } + append_page(page, &menu->pages, &pages_end); + } + } else { + // Calculate available space + int max_width = menu->width - menu->inputw - menu->promptw + - menu->left_arrow - menu->right_arrow; + + struct page *pages_end = NULL; + struct item *item = menu->matches; + while (item) { + struct page *page = calloc(1, sizeof(struct page)); + page->first = item; + + int total_width = 0; + int items = 0; + while (item) { + total_width += item->width + 2 * menu->padding; + if (total_width > max_width && items > 0) { + break; + } + items++; + + item->page = page; + page->last = item; + item = item->next_match; + } + append_page(page, &menu->pages, &pages_end); + } + } +} + +static const char *fstrstr(struct menu *menu, const char *s, const char *sub) { + for (size_t len = strlen(sub); *s; s++) { + if (!menu->strncmp(s, sub, len)) { + return s; + } + } + return NULL; +} + +static void append_match(struct item *item, struct item **first, struct item **last) { + if (*last) { + (*last)->next_match = item; + } else { + *first = item; + } + item->prev_match = *last; + item->next_match = NULL; + *last = item; +} + +static void match_items(struct menu *menu) { + struct item *lexact = NULL, *exactend = NULL; + struct item *lprefix = NULL, *prefixend = NULL; + struct item *lsubstr = NULL, *substrend = NULL; + 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; + menu->sel = NULL; + + size_t input_len = strlen(menu->input); + + /* tokenize input by space for matching the tokens individually */ + strcpy(buf, menu->input); + tok = strtok(buf, " "); + while (tok) { + tokv = realloc(tokv, (tokc + 1) * sizeof *tokv); + if (!tokv) { + fprintf(stderr, "could not realloc %zu bytes", + (tokc + 1) * sizeof *tokv); + exit(EXIT_FAILURE); + } + tokv[tokc] = tok; + tokc++; + tok = strtok(NULL, " "); + } + tok_len = tokc ? strlen(tokv[0]) : 0; + + 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 */ + break; + } + } + if (i != tokc) { + /* not all tokens match */ + continue; + } + if (!tokc || !menu->strncmp(menu->input, item->text, input_len + 1)) { + append_match(item, &lexact, &exactend); + } else if (!menu->strncmp(tokv[0], item->text, tok_len)) { + append_match(item, &lprefix, &prefixend); + } else { + append_match(item, &lsubstr, &substrend); + } + } + + free(tokv); + + if (lexact) { + menu->matches = lexact; + menu->matches_end = exactend; + } + if (lprefix) { + if (menu->matches_end) { + menu->matches_end->next_match = lprefix; + lprefix->prev_match = menu->matches_end; + } else { + menu->matches = lprefix; + } + menu->matches_end = prefixend; + } + if (lsubstr) { + if (menu->matches_end) { + menu->matches_end->next_match = lsubstr; + lsubstr->prev_match = menu->matches_end; + } else { + menu->matches = lsubstr; + } + menu->matches_end = substrend; + } + + page_items(menu); + if (menu->pages) { + menu->sel = menu->pages->first; + } +} + +// 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) { + calc_widths(menu); + match_items(menu); + render_menu(menu); +} + +static void insert(struct menu *menu, const char *text, ssize_t len) { + if (strlen(menu->input) + len > sizeof menu->input - 1) { + return; + } + memmove(menu->input + menu->cursor + len, menu->input + menu->cursor, + sizeof menu->input - menu->cursor - MAX(len, 0)); + if (len > 0 && text != NULL) { + memcpy(menu->input + menu->cursor, text, len); + } + menu->cursor += len; +} + +// Add pasted text to the menu input. +void menu_paste(struct menu *menu, const char *text, ssize_t len) { + insert(menu, text, len); +} + +static size_t nextrune(struct menu *menu, int incr) { + size_t n, len; + + len = strlen(menu->input); + for(n = menu->cursor + incr; n < len && (menu->input[n] & 0xc0) == 0x80; n += incr); + return n; +} + +// Move the cursor to the beginning or end of the word, skipping over any preceding whitespace. +static void movewordedge(struct menu *menu, int dir) { + if (dir < 0) { + // Move to beginning of word + while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] == ' ') { + menu->cursor = nextrune(menu, -1); + } + while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] != ' ') { + menu->cursor = nextrune(menu, -1); + } + } else { + // Move to end of word + size_t len = strlen(menu->input); + while (menu->cursor < len && menu->input[menu->cursor] == ' ') { + menu->cursor = nextrune(menu, +1); + } + while (menu->cursor < len && menu->input[menu->cursor] != ' ') { + menu->cursor = nextrune(menu, +1); + } + } +} + +// Handle a keypress. +void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state, + xkb_keysym_t sym) { + if (key_state != WL_KEYBOARD_KEY_STATE_PRESSED) { + return; + } + + struct xkb_state *state = context_get_xkb_state(menu->context); + bool ctrl = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + bool meta = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + bool shift = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + + size_t len = strlen(menu->input); + + if (ctrl) { + // Emacs-style line editing bindings + switch (sym) { + case XKB_KEY_a: + sym = XKB_KEY_Home; + break; + case XKB_KEY_b: + sym = XKB_KEY_Left; + break; + case XKB_KEY_c: + sym = XKB_KEY_Escape; + break; + case XKB_KEY_d: + sym = XKB_KEY_Delete; + break; + case XKB_KEY_e: + sym = XKB_KEY_End; + break; + case XKB_KEY_f: + sym = XKB_KEY_Right; + break; + case XKB_KEY_g: + sym = XKB_KEY_Escape; + break; + case XKB_KEY_bracketleft: + sym = XKB_KEY_Escape; + break; + case XKB_KEY_h: + sym = XKB_KEY_BackSpace; + break; + case XKB_KEY_i: + sym = XKB_KEY_Tab; + break; + case XKB_KEY_j: + case XKB_KEY_J: + case XKB_KEY_m: + case XKB_KEY_M: + sym = XKB_KEY_Return; + ctrl = false; + break; + case XKB_KEY_n: + sym = XKB_KEY_Down; + break; + case XKB_KEY_p: + sym = XKB_KEY_Up; + break; + + case XKB_KEY_k: + // Delete right + menu->input[menu->cursor] = '\0'; + match_items(menu); + menu_invalidate(menu); + return; + case XKB_KEY_u: + // Delete left + insert(menu, NULL, 0 - menu->cursor); + match_items(menu); + menu_invalidate(menu); + return; + case XKB_KEY_w: + // Delete word + while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] == ' ') { + insert(menu, NULL, nextrune(menu, -1) - menu->cursor); + } + while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] != ' ') { + insert(menu, NULL, nextrune(menu, -1) - menu->cursor); + } + match_items(menu); + menu_invalidate(menu); + return; + case XKB_KEY_Y: + // Paste clipboard + if (!context_paste(menu->context)) { + return; + } + match_items(menu); + menu_invalidate(menu); + return; + case XKB_KEY_Left: + case XKB_KEY_KP_Left: + movewordedge(menu, -1); + menu_invalidate(menu); + return; + case XKB_KEY_Right: + case XKB_KEY_KP_Right: + movewordedge(menu, +1); + menu_invalidate(menu); + return; + + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + break; + default: + return; + } + } else if (meta) { + // Emacs-style line editing bindings + switch (sym) { + case XKB_KEY_b: + movewordedge(menu, -1); + menu_invalidate(menu); + return; + case XKB_KEY_f: + movewordedge(menu, +1); + menu_invalidate(menu); + return; + case XKB_KEY_g: + sym = XKB_KEY_Home; + break; + case XKB_KEY_G: + sym = XKB_KEY_End; + break; + case XKB_KEY_h: + sym = XKB_KEY_Up; + break; + case XKB_KEY_j: + sym = XKB_KEY_Next; + break; + case XKB_KEY_k: + sym = XKB_KEY_Prior; + break; + case XKB_KEY_l: + sym = XKB_KEY_Down; + break; + default: + return; + } + } + + char buf[8]; + switch (sym) { + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + if (shift) { + menu->callback(menu, menu->input, true); + } else { + char *text = menu->sel ? menu->sel->text : menu->input; + menu->callback(menu, text, !ctrl); + } + break; + case XKB_KEY_Left: + case XKB_KEY_KP_Left: + case XKB_KEY_Up: + case XKB_KEY_KP_Up: + if (menu->sel && menu->sel->prev_match) { + menu->sel = menu->sel->prev_match; + menu_invalidate(menu); + } else if (menu->cursor > 0) { + menu->cursor = nextrune(menu, -1); + menu_invalidate(menu); + } + break; + case XKB_KEY_Right: + case XKB_KEY_KP_Right: + case XKB_KEY_Down: + case XKB_KEY_KP_Down: + if (menu->cursor < len) { + menu->cursor = nextrune(menu, +1); + menu_invalidate(menu); + } else if (menu->sel && menu->sel->next_match) { + menu->sel = menu->sel->next_match; + 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; + 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; + menu_invalidate(menu); + } + break; + case XKB_KEY_Home: + case XKB_KEY_KP_Home: + if (menu->sel == menu->matches) { + menu->cursor = 0; + menu_invalidate(menu); + } else { + menu->sel = menu->matches; + menu_invalidate(menu); + } + break; + case XKB_KEY_End: + case XKB_KEY_KP_End: + if (menu->cursor < len) { + menu->cursor = len; + menu_invalidate(menu); + } else { + menu->sel = menu->matches_end; + menu_invalidate(menu); + } + break; + case XKB_KEY_BackSpace: + if (menu->cursor > 0) { + insert(menu, NULL, nextrune(menu, -1) - menu->cursor); + match_items(menu); + menu_invalidate(menu); + } + break; + case XKB_KEY_Delete: + case XKB_KEY_KP_Delete: + if (menu->cursor == len) { + return; + } + menu->cursor = nextrune(menu, +1); + insert(menu, NULL, nextrune(menu, -1) - menu->cursor); + match_items(menu); + menu_invalidate(menu); + break; + case XKB_KEY_Tab: + if (!menu->sel) { + return; + } + menu->cursor = strnlen(menu->sel->text, sizeof menu->input - 1); + memcpy(menu->input, menu->sel->text, menu->cursor); + menu->input[menu->cursor] = '\0'; + match_items(menu); + menu_invalidate(menu); + break; + case XKB_KEY_Escape: + menu->exit = true; + menu->failure = true; + break; + default: + if (xkb_keysym_to_utf8(sym, buf, 8)) { + insert(menu, buf, strnlen(buf, 8)); + match_items(menu); + menu_invalidate(menu); + } + } +} diff --git a/menu.h b/menu.h @@ -0,0 +1,95 @@ +#ifndef WMENU_MENU_H +#define WMENU_MENU_H + +#include <cairo/cairo.h> +#include <stdbool.h> +#include <sys/types.h> +#include <xkbcommon/xkbcommon.h> +#include <wayland-client.h> + +struct menu; +typedef void (*menu_callback)(struct menu *menu, char *text, bool exit); + +// A menu item. +struct item { + char *text; + int width; + struct item *prev_match; // previous matching item + struct item *next_match; // next matching item + struct page *page; // the page holding this item +}; + +// A page of menu items. +struct page { + struct item *first; // first item in the page + struct item *last; // last item in the page + struct page *prev; // previous page + struct page *next; // next page +}; + +// Menu state. +struct menu { + // Whether the menu appears at the bottom of the screen + 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 + int lines; + // The name of the output to display on + char *output_name; + // The prompt displayed to the left of the input field + char *prompt; + // Normal colors + uint32_t normalbg, normalfg; + // Prompt colors + uint32_t promptbg, promptfg; + // Selection colors + uint32_t selectionbg, selectionfg; + + 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; + int padding; + int inputw; + int promptw; + int left_arrow; + int right_arrow; + bool rendered; + + char input[BUFSIZ]; + size_t cursor; + + 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 + + menu_callback callback; + bool exit; + bool failure; +}; + +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); +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, + xkb_keysym_t sym); + +#endif diff --git a/meson.build b/meson.build @@ -0,0 +1,80 @@ +project( + 'wmenu', + 'c', + version: '0.1.9', + license: 'MIT', + default_options: [ + 'c_std=c11', + 'warning_level=2', + 'werror=true', + ] +) + +cc = meson.get_compiler('c') + +add_project_arguments(cc.get_supported_arguments([ + '-DVERSION="@0@"'.format(meson.project_version()), + + '-Wno-missing-field-initializers', + '-Wno-unused-parameter', + '-Wundef', + '-Wvla', +]), language : 'c') + +cairo = dependency('cairo') +pango = dependency('pango') +pangocairo = dependency('pangocairo') +wayland_client = dependency('wayland-client') +wayland_protos = dependency('wayland-protocols') +xkbcommon = dependency('xkbcommon') + +rt = cc.find_library('rt') + +subdir('protocols') +subdir('docs') + +executable( + 'wmenu', + files( + 'menu.c', + 'pango.c', + 'pool-buffer.c', + 'render.c', + 'wayland.c', + 'wmenu.c', + ), + dependencies: [ + cairo, + client_protos, + pango, + pangocairo, + rt, + wayland_client, + wayland_protos, + xkbcommon, + ], + install: true, +) + +executable( + 'wmenu-run', + files( + 'menu.c', + 'pango.c', + 'pool-buffer.c', + 'render.c', + 'wayland.c', + 'wmenu-run.c', + ), + dependencies: [ + cairo, + client_protos, + pango, + pangocairo, + rt, + wayland_client, + wayland_protos, + xkbcommon, + ], + install: true, +) diff --git a/pango.c b/pango.c @@ -0,0 +1,71 @@ +#include <cairo/cairo.h> +#include <pango/pangocairo.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "pango.h" + +int get_font_height(const char *fontstr) { + PangoFontMap *fontmap = pango_cairo_font_map_get_default(); + PangoContext *context = pango_font_map_create_context(fontmap); + PangoFontDescription *desc = pango_font_description_from_string(fontstr); + PangoFont *font = pango_font_map_load_font(fontmap, context, desc); + if (font == NULL) { + pango_font_description_free(desc); + g_object_unref(context); + return -1; + } + PangoFontMetrics *metrics = pango_font_get_metrics(font, NULL); + int height = pango_font_metrics_get_height(metrics) / PANGO_SCALE; + pango_font_metrics_unref(metrics); + g_object_unref(font); + pango_font_description_free(desc); + g_object_unref(context); + return height; +} + +PangoLayout *get_pango_layout(cairo_t *cairo, const char *font, + const char *text, double scale) { + PangoLayout *layout = pango_cairo_create_layout(cairo); + PangoAttrList *attrs = pango_attr_list_new(); + pango_layout_set_text(layout, text, -1); + pango_attr_list_insert(attrs, pango_attr_scale_new(scale)); + PangoFontDescription *desc = pango_font_description_from_string(font); + pango_layout_set_font_description(layout, desc); + pango_layout_set_single_paragraph_mode(layout, 1); + pango_layout_set_attributes(layout, attrs); + pango_font_description_free(desc); + pango_attr_list_unref(attrs); + return layout; +} + +void get_text_size(cairo_t *cairo, const char *font, int *width, int *height, + int *baseline, double scale, const char *text) { + PangoLayout *layout = get_pango_layout(cairo, font, text, scale); + 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); +} + +int text_width(cairo_t *cairo, const char *font, const char *text) { + int text_width; + get_text_size(cairo, font, &text_width, NULL, NULL, 1, text); + return text_width; +} + +void pango_printf(cairo_t *cairo, const char *font, double scale, + const char *text) { + PangoLayout *layout = get_pango_layout(cairo, font, text, scale); + 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); +} diff --git a/pango.h b/pango.h @@ -0,0 +1,16 @@ +#ifndef WMENU_PANGO_H +#define WMENU_PANGO_H +#include <stdbool.h> +#include <cairo/cairo.h> +#include <pango/pangocairo.h> + +int get_font_height(const char *font); +PangoLayout *get_pango_layout(cairo_t *cairo, const char *font, + const char *text, double scale); +void get_text_size(cairo_t *cairo, const char *font, int *width, int *height, + int *baseline, double scale, const char *text); +int text_width(cairo_t *cairo, const char *font, const char *text); +void pango_printf(cairo_t *cairo, const char *font, double scale, + const char *text); + +#endif diff --git a/pool-buffer.c b/pool-buffer.c @@ -0,0 +1,146 @@ +#define _POSIX_C_SOURCE 200809L +#include <assert.h> +#include <cairo.h> +#include <errno.h> +#include <fcntl.h> +#include <pango/pangocairo.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <time.h> +#include <unistd.h> +#include <wayland-client.h> + +#include "pool-buffer.h" + +static void randname(char *buf) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + long r = ts.tv_nsec; + for (int i = 0; i < 6; ++i) { + buf[i] = 'A'+(r&15)+(r&16)*2; + r >>= 5; + } +} + +static int anonymous_shm_open(void) { + char name[] = "/wmenu-XXXXXX"; + int retries = 100; + + do { + randname(name + strlen(name) - 6); + + --retries; + // shm_open guarantees that O_CLOEXEC is set + int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + return fd; + } + } while (retries > 0 && errno == EEXIST); + + return -1; +} + +static int create_shm_file(off_t size) { + int fd = anonymous_shm_open(); + if (fd < 0) { + return fd; + } + + if (ftruncate(fd, size) < 0) { + close(fd); + return -1; + } + + return fd; +} + +static void buffer_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, + int32_t scale, uint32_t format) { + int32_t stride = width * scale * 4; + int32_t size = stride * height * scale; + + int fd = create_shm_file(size); + assert(fd != -1); + void *data = mmap(NULL, (size_t)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 * scale, height * scale, stride, format); + wl_shm_pool_destroy(pool); + close(fd); + + buf->size = (size_t)size; + buf->width = width; + buf->height = height; + buf->scale = scale; + buf->data = data; + buf->surface = cairo_image_surface_create_for_data(data, + CAIRO_FORMAT_ARGB32, width * scale, height * scale, stride); + cairo_surface_set_device_scale(buf->surface, scale, scale); + 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); + } + if (buffer->cairo) { + cairo_destroy(buffer->cairo); + } + if (buffer->surface) { + cairo_surface_destroy(buffer->surface); + } + if (buffer->pango) { + g_object_unref(buffer->pango); + } + if (buffer->data) { + munmap(buffer->data, buffer->size); + } + memset(buffer, 0, sizeof(struct pool_buffer)); +} + +struct pool_buffer *get_next_buffer(struct wl_shm *shm, + struct pool_buffer pool[static 2], int32_t width, int32_t height, int32_t scale) { + 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 + || buffer->scale != scale) { + destroy_buffer(buffer); + } + + if (!buffer->buffer) { + if (!create_buffer(shm, buffer, width, height, scale, + WL_SHM_FORMAT_ARGB8888)) { + return NULL; + } + } + buffer->busy = true; + return buffer; +} diff --git a/pool-buffer.h b/pool-buffer.h @@ -0,0 +1,26 @@ +/* Taken from sway. MIT licensed */ +#ifndef WMENU_POOL_BUFFER_H +#define WMENU_POOL_BUFFER_H + +#include <cairo.h> +#include <pango/pangocairo.h> +#include <stdbool.h> +#include <stdint.h> +#include <wayland-client.h> + +struct pool_buffer { + struct wl_buffer *buffer; + cairo_surface_t *surface; + cairo_t *cairo; + PangoContext *pango; + size_t size; + int32_t width, height, scale; + bool busy; + void *data; +}; + +struct pool_buffer *get_next_buffer(struct wl_shm *shm, + struct pool_buffer pool[static 2], int32_t width, int32_t height, int32_t scale); +void destroy_buffer(struct pool_buffer *buffer); + +#endif diff --git a/protocols/meson.build b/protocols/meson.build @@ -0,0 +1,47 @@ +wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') + +wayland_scanner_dep = dependency('wayland-scanner', required: false, native: true) +if wayland_scanner_dep.found() + wayland_scanner = find_program( + wayland_scanner_dep.get_pkgconfig_variable('wayland_scanner'), + native: true, + ) +else + wayland_scanner = find_program('wayland-scanner', native: true) +endif + +protocols = [ + [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], + [wl_protocol_dir, 'staging/xdg-activation/xdg-activation-v1.xml'], + ['wlr-layer-shell-unstable-v1.xml'], +] + +wl_protos_src = [] +wl_protos_headers = [] + +foreach p : protocols + xml = join_paths(p) + wl_protos_src += custom_target( + xml.underscorify() + '_protocol_c', + input: xml, + output: '@BASENAME@-protocol.c', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) + wl_protos_headers += custom_target( + xml.underscorify() + '_client_h', + input: xml, + output: '@BASENAME@-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) +endforeach + +lib_client_protos = static_library( + 'client_protos', + wl_protos_src + wl_protos_headers, + dependencies: wayland_client.partial_dependency(compile_args: true), +) + +client_protos = declare_dependency( + link_with: lib_client_protos, + sources: wl_protos_headers, +) diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,285 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="wlr_layer_shell_unstable_v1"> + <copyright> + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + </copyright> + + <interface name="zwlr_layer_shell_v1" version="1"> + <description summary="create surfaces that are layers of the desktop"> + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + </description> + + <request name="get_layer_surface"> + <description summary="create a layer_surface from a surface"> + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + </description> + <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/> + <arg name="surface" type="object" interface="wl_surface"/> + <arg name="output" type="object" interface="wl_output" allow-null="true"/> + <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/> + <arg name="namespace" type="string" summary="namespace for the layer surface"/> + </request> + + <enum name="error"> + <entry name="role" value="0" summary="wl_surface has another role"/> + <entry name="invalid_layer" value="1" summary="layer value is invalid"/> + <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/> + </enum> + + <enum name="layer"> + <description summary="available layers for surfaces"> + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + </description> + + <entry name="background" value="0"/> + <entry name="bottom" value="1"/> + <entry name="top" value="2"/> + <entry name="overlay" value="3"/> + </enum> + </interface> + + <interface name="zwlr_layer_surface_v1" version="1"> + <description summary="layer metadata interface"> + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (size, anchor, exclusive zone, margin, interactivity) + is double-buffered, and will be applied at the time wl_surface.commit of + the corresponding wl_surface is called. + </description> + + <request name="set_size"> + <description summary="sets the size of the surface"> + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + </description> + <arg name="width" type="uint"/> + <arg name="height" type="uint"/> + </request> + + <request name="set_anchor"> + <description summary="configures the anchor point of the surface"> + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthoginal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + </description> + <arg name="anchor" type="uint" enum="anchor"/> + </request> + + <request name="set_exclusive_zone"> + <description summary="configures the exclusive geometry of this surface"> + Requests that the compositor avoids occluding an area of the surface + with other surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to an + edge, rather than a corner. The zone is the number of surface-local + coordinates from the edge that are considered exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive excluzive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + </description> + <arg name="zone" type="int"/> + </request> + + <request name="set_margin"> + <description summary="sets a margin from the anchor point"> + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + </description> + <arg name="top" type="int"/> + <arg name="right" type="int"/> + <arg name="bottom" type="int"/> + <arg name="left" type="int"/> + </request> + + <request name="set_keyboard_interactivity"> + <description summary="requests keyboard events"> + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + </description> + <arg name="keyboard_interactivity" type="uint"/> + </request> + + <request name="get_popup"> + <description summary="assign this layer_surface as an xdg_popup parent"> + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + </description> + <arg name="popup" type="object" interface="xdg_popup"/> + </request> + + <request name="ack_configure"> + <description summary="ack a configure event"> + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + </description> + <arg name="serial" type="uint" summary="the serial from the configure event"/> + </request> + + <request name="destroy" type="destructor"> + <description summary="destroy the layer_surface"> + This request destroys the layer surface. + </description> + </request> + + <event name="configure"> + <description summary="suggest a surface change"> + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + </description> + <arg name="serial" type="uint"/> + <arg name="width" type="uint"/> + <arg name="height" type="uint"/> + </event> + + <event name="closed"> + <description summary="surface should be closed"> + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + </description> + </event> + + <enum name="error"> + <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/> + <entry name="invalid_size" value="1" summary="size is invalid"/> + <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/> + </enum> + + <enum name="anchor" bitfield="true"> + <entry name="top" value="1" summary="the top edge of the anchor rectangle"/> + <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/> + <entry name="left" value="4" summary="the left edge of the anchor rectangle"/> + <entry name="right" value="8" summary="the right edge of the anchor rectangle"/> + </enum> + </interface> +</protocol> diff --git a/render.c b/render.c @@ -0,0 +1,212 @@ +#define _POSIX_C_SOURCE 200809L +#include <cairo/cairo.h> +#include <stdlib.h> +#include <string.h> + +#include "render.h" + +#include "menu.h" +#include "pango.h" +#include "pool-buffer.h" +#include "wayland.h" + +// Calculate text widths. +void calc_widths(struct menu *menu) { + struct wl_context *context = menu->context; + 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) { + menu->promptw = text_width(cairo, menu->font, menu->prompt) + menu->padding + menu->padding/2; + } else { + menu->promptw = 0; + } + + // Calculate scroll indicator widths + menu->left_arrow = text_width(cairo, menu->font, "<") + 2 * menu->padding; + menu->right_arrow = text_width(cairo, menu->font, ">") + 2 * menu->padding; + + // Calculate item widths and input area width + 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; + } + } +} + +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); +} + +// Renders text to cairo. +static int render_text(struct menu *menu, cairo_t *cairo, const char *str, + int x, int y, int width, uint32_t bg_color, uint32_t fg_color, + int left_padding, int right_padding) { + + int text_width, text_height; + get_text_size(cairo, menu->font, &text_width, &text_height, NULL, 1, str); + int text_y = (menu->line_height / 2.0) - (text_height / 2.0); + + if (width == 0) { + width = text_width + left_padding + right_padding; + } + if (bg_color) { + cairo_set_source_u32(cairo, bg_color); + cairo_rectangle(cairo, x, y, width, menu->line_height); + cairo_fill(cairo); + } + cairo_move_to(cairo, x + left_padding, y + text_y); + cairo_set_source_u32(cairo, fg_color); + pango_printf(cairo, menu->font, 1, str); + + return width; +} + +// Renders the prompt message. +static void render_prompt(struct menu *menu, cairo_t *cairo) { + if (!menu->prompt) { + return; + } + render_text(menu, cairo, menu->prompt, 0, 0, 0, + menu->promptbg, menu->promptfg, menu->padding, menu->padding/2); +} + +// Renders the input text. +static void render_input(struct menu *menu, cairo_t *cairo) { + 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. +static void render_cursor(struct menu *menu, cairo_t *cairo) { + const int cursor_width = 2; + const int cursor_margin = 2; + int cursor_pos = menu->promptw + menu->padding + + text_width(cairo, menu->font, menu->input) + - text_width(cairo, menu->font, &menu->input[menu->cursor]) + - cursor_width / 2; + cairo_rectangle(cairo, cursor_pos, cursor_margin, cursor_width, + menu->line_height - 2 * cursor_margin); + cairo_fill(cairo); +} + +// Renders a single menu item horizontally. +static int render_horizontal_item(struct menu *menu, cairo_t *cairo, struct item *item, int x) { + uint32_t bg_color = menu->sel == item ? menu->selectionbg : menu->normalbg; + uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->normalfg; + + return render_text(menu, cairo, item->text, x, 0, 0, + bg_color, fg_color, menu->padding, menu->padding); +} + +// Renders a single menu item vertically. +static int render_vertical_item(struct menu *menu, cairo_t *cairo, struct item *item, int x, int y) { + uint32_t bg_color = menu->sel == item ? menu->selectionbg : menu->normalbg; + uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->normalfg; + + render_text(menu, cairo, item->text, x, y, menu->width - x, + bg_color, fg_color, menu->padding, 0); + return menu->line_height; +} + +// Renders a page of menu items horizontally. +static void render_horizontal_page(struct menu *menu, cairo_t *cairo, struct page *page) { + int x = menu->promptw + menu->inputw + menu->left_arrow; + for (struct item *item = page->first; item != page->last->next_match; item = item->next_match) { + x += render_horizontal_item(menu, cairo, item, x); + } + + // Draw left and right scroll indicators if necessary + if (page->prev) { + cairo_move_to(cairo, menu->promptw + menu->inputw + menu->padding, 0); + pango_printf(cairo, menu->font, 1, "<"); + } + if (page->next) { + cairo_move_to(cairo, menu->width - menu->right_arrow + menu->padding, 0); + pango_printf(cairo, menu->font, 1, ">"); + } +} + +// Renders a page of menu items vertically. +static void render_vertical_page(struct menu *menu, cairo_t *cairo, struct page *page) { + int x = menu->promptw; + int y = menu->line_height; + for (struct item *item = page->first; item != page->last->next_match; item = item->next_match) { + y += render_vertical_item(menu, cairo, item, x, y); + } +} + +// Renders the menu to cairo. +static void render_to_cairo(struct menu *menu, cairo_t *cairo) { + // Render background + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + cairo_set_source_u32(cairo, menu->normalbg); + cairo_paint(cairo); + + // Render prompt and input + render_prompt(menu, cairo); + render_input(menu, cairo); + render_cursor(menu, cairo); + + // Render selected page + if (!menu->sel) { + return; + } + if (menu->lines > 0) { + render_vertical_page(menu, cairo, menu->sel->page); + } else { + render_horizontal_page(menu, cairo, menu->sel->page); + } +} + +// Renders a single frame of the menu. +void render_menu(struct menu *menu) { + struct wl_context *context = menu->context; + + int scale = context_get_scale(context); + struct pool_buffer *buffer = context_get_next_buffer(context, scale); + if (!buffer) { + return; + } + + cairo_t *shm = buffer->cairo; + 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); + wl_surface_attach(surface, buffer->buffer, 0, 0); + wl_surface_damage(surface, 0, 0, menu->width, menu->height); + wl_surface_commit(surface); + + menu->rendered = true; +} diff --git a/render.h b/render.h @@ -0,0 +1,9 @@ +#ifndef WMENU_RENDER_H +#define WMENU_RENDER_H + +#include "menu.h" + +void calc_widths(struct menu *menu); +void render_menu(struct menu *menu); + +#endif diff --git a/wayland.c b/wayland.c @@ -0,0 +1,510 @@ +#define _POSIX_C_SOURCE 200809L +#include <assert.h> +#include <errno.h> +#include <poll.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <time.h> +#include <unistd.h> +#include <sys/mman.h> +#include <sys/timerfd.h> +#include <wayland-client.h> +#include <wayland-client-protocol.h> +#include <xkbcommon/xkbcommon.h> + +#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" + +// A Wayland output. +struct output { + struct wl_context *context; + struct wl_output *output; + const char *name; // output name + int32_t scale; // output scale + struct output *next; // next output +}; + +// Creates and returns a new output. +static struct output *output_create(struct wl_context *context, struct wl_output *wl_output) { + struct output *output = calloc(1, sizeof(struct output)); + output->context = context; + output->output = wl_output; + output->scale = 1; + return output; +} + +// Keyboard state. +struct keyboard { + struct menu *menu; + struct wl_keyboard *keyboard; + struct xkb_context *context; + struct xkb_keymap *keymap; + struct xkb_state *state; + + int repeat_timer; + int repeat_delay; + int repeat_period; + enum wl_keyboard_key_state repeat_key_state; + xkb_keysym_t repeat_sym; +}; + +// Creates and returns a new keyboard. +static struct keyboard *keyboard_create(struct menu *menu, struct wl_keyboard *wl_keyboard) { + struct keyboard *keyboard = calloc(1, sizeof(struct keyboard)); + keyboard->menu = menu; + keyboard->keyboard = wl_keyboard; + keyboard->context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + assert(keyboard->context != NULL); + keyboard->repeat_timer = timerfd_create(CLOCK_MONOTONIC, 0); + assert(keyboard->repeat_timer != -1); + return keyboard; +} + +// Frees the keyboard. +static void free_keyboard(struct keyboard *keyboard) { + wl_keyboard_release(keyboard->keyboard); + xkb_state_unref(keyboard->state); + xkb_keymap_unref(keyboard->keymap); + xkb_context_unref(keyboard->context); + free(keyboard); +} + +// Wayland context. +struct wl_context { + struct menu *menu; + + struct wl_display *display; + struct wl_registry *registry; + struct wl_compositor *compositor; + struct wl_shm *shm; + struct wl_seat *seat; + struct wl_data_device_manager *data_device_manager; + struct zwlr_layer_shell_v1 *layer_shell; + struct output *output_list; + struct xdg_activation_v1 *activation; + + struct keyboard *keyboard; + struct wl_data_device *data_device; + struct wl_surface *surface; + struct zwlr_layer_surface_v1 *layer_surface; + struct wl_data_offer *data_offer; + struct output *output; + + struct pool_buffer buffers[2]; + struct pool_buffer *current; +}; + +// Returns the current output_scale. +int context_get_scale(struct wl_context *context) { + return context->output ? context->output->scale : 1; +} + +// Returns the current buffer from the pool. +struct pool_buffer *context_get_current_buffer(struct wl_context *context) { + return context->current; +} + +// Returns the next buffer from the pool. +struct pool_buffer *context_get_next_buffer(struct wl_context *context, int scale) { + struct menu *menu = context->menu; + context->current = get_next_buffer(context->shm, context->buffers, menu->width, menu->height, scale); + return context->current; +} + +// Returns the Wayland surface for the context. +struct wl_surface *context_get_surface(struct wl_context *context) { + return context->surface; +} + +// Returns the XKB state for the context. +struct xkb_state *context_get_xkb_state(struct wl_context *context) { + return context->keyboard->state; +} + +// Returns the XDG activation object for the context. +struct xdg_activation_v1 *context_get_xdg_activation(struct wl_context *context) { + return context->activation; +} + +// Retrieves pasted text from a Wayland data offer. +bool context_paste(struct wl_context *context) { + if (!context->data_offer) { + return false; + } + + int fds[2]; + if (pipe(fds) == -1) { + // Pipe failed + return false; + } + wl_data_offer_receive(context->data_offer, "text/plain", fds[1]); + close(fds[1]); + + wl_display_roundtrip(context->display); + + while (true) { + char buf[1024]; + ssize_t n = read(fds[0], buf, sizeof(buf)); + if (n <= 0) { + break; + } + menu_paste(context->menu, buf, n); + } + close(fds[0]); + + wl_data_offer_destroy(context->data_offer); + context->data_offer = NULL; + return true; +} + +// Adds an output to the output list. +static void context_add_output(struct wl_context *context, struct output *output) { + output->next = context->output_list; + context->output_list = output; +} + +// Frees the outputs. +static void free_outputs(struct wl_context *context) { + struct output *next = context->output_list; + while (next) { + struct output *output = next; + next = output->next; + wl_output_destroy(output->output); + free(output); + } +} + +// Destroys the Wayland context, freeing memory associated with it. +static void context_destroy(struct wl_context *context) { + wl_registry_destroy(context->registry); + wl_compositor_destroy(context->compositor); + wl_shm_destroy(context->shm); + wl_seat_destroy(context->seat); + wl_data_device_manager_destroy(context->data_device_manager); + zwlr_layer_shell_v1_destroy(context->layer_shell); + free_outputs(context); + + free_keyboard(context->keyboard); + wl_data_device_destroy(context->data_device); + wl_surface_destroy(context->surface); + zwlr_layer_surface_v1_destroy(context->layer_surface); + xdg_activation_v1_destroy(context->activation); + + wl_display_disconnect(context->display); + free(context); +} + +static void noop() { + // Do nothing +} + +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 = { + .enter = surface_enter, + .leave = noop, +}; + +static void layer_surface_configure(void *data, + struct zwlr_layer_surface_v1 *surface, + uint32_t serial, uint32_t width, uint32_t height) { + struct wl_context *context = data; + context->menu->width = width; + context->menu->height = height; + zwlr_layer_surface_v1_ack_configure(surface, serial); +} + +static void layer_surface_closed(void *data, struct zwlr_layer_surface_v1 *surface) { + struct wl_context *context = data; + context->menu->exit = true; +} + +static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + +static void output_scale(void *data, struct wl_output *wl_output, int32_t factor) { + struct output *output = data; + output->scale = factor; +} + +static void output_name(void *data, struct wl_output *wl_output, const char *name) { + struct output *output = data; + output->name = name; + + struct wl_context *context = output->context; + if (context->menu->output_name && strcmp(context->menu->output_name, name) == 0) { + context->output = output; + } +} + +static const struct wl_output_listener output_listener = { + .geometry = noop, + .mode = noop, + .done = noop, + .scale = output_scale, + .name = output_name, + .description = noop, +}; + +static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) { + struct keyboard *keyboard = data; + assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1); + + char *map_shm = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + assert(map_shm != MAP_FAILED); + + keyboard->keymap = xkb_keymap_new_from_string(keyboard->context, + map_shm, XKB_KEYMAP_FORMAT_TEXT_V1, 0); + munmap(map_shm, size); + close(fd); + + keyboard->state = xkb_state_new(keyboard->keymap); +} + +static void keyboard_repeat(struct keyboard *keyboard) { + menu_keypress(keyboard->menu, keyboard->repeat_key_state, keyboard->repeat_sym); + struct itimerspec spec = { 0 }; + spec.it_value.tv_sec = keyboard->repeat_period / 1000; + spec.it_value.tv_nsec = (keyboard->repeat_period % 1000) * 1000000l; + timerfd_settime(keyboard->repeat_timer, 0, &spec, NULL); +} + +static void keyboard_key(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t time, uint32_t key, uint32_t _key_state) { + struct keyboard *keyboard = data; + + enum wl_keyboard_key_state key_state = _key_state; + xkb_keysym_t sym = xkb_state_key_get_one_sym(keyboard->state, key + 8); + menu_keypress(keyboard->menu, key_state, sym); + + if (key_state == WL_KEYBOARD_KEY_STATE_PRESSED && keyboard->repeat_period >= 0) { + keyboard->repeat_key_state = key_state; + keyboard->repeat_sym = sym; + + struct itimerspec spec = { 0 }; + spec.it_value.tv_sec = keyboard->repeat_delay / 1000; + spec.it_value.tv_nsec = (keyboard->repeat_delay % 1000) * 1000000l; + timerfd_settime(keyboard->repeat_timer, 0, &spec, NULL); + } else if (key_state == WL_KEYBOARD_KEY_STATE_RELEASED) { + struct itimerspec spec = { 0 }; + timerfd_settime(keyboard->repeat_timer, 0, &spec, NULL); + } +} + +static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) { + struct keyboard *keyboard = data; + keyboard->repeat_delay = delay; + if (rate > 0) { + keyboard->repeat_period = 1000 / rate; + } else { + keyboard->repeat_period = -1; + } +} + +static void keyboard_modifiers(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, + uint32_t group) { + struct keyboard *keyboard = data; + xkb_state_update_mask(keyboard->state, mods_depressed, mods_latched, + mods_locked, 0, 0, group); +} + +static const struct wl_keyboard_listener keyboard_listener = { + .keymap = keyboard_keymap, + .enter = noop, + .leave = noop, + .key = keyboard_key, + .modifiers = keyboard_modifiers, + .repeat_info = keyboard_repeat_info, +}; + +static void seat_capabilities(void *data, struct wl_seat *seat, + enum wl_seat_capability caps) { + struct wl_context *context = data; + if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { + struct wl_keyboard *wl_keyboard = wl_seat_get_keyboard(seat); + struct keyboard *keyboard = keyboard_create(context->menu, wl_keyboard); + wl_keyboard_add_listener(wl_keyboard, &keyboard_listener, keyboard); + context->keyboard = keyboard; + } +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_capabilities, + .name = noop, +}; + +static void data_device_selection(void *data, struct wl_data_device *data_device, + struct wl_data_offer *data_offer) { + struct wl_context *context = data; + context->data_offer = data_offer; +} + +static const struct wl_data_device_listener data_device_listener = { + .data_offer = noop, + .enter = noop, + .leave = noop, + .motion = noop, + .drop = noop, + .selection = data_device_selection, +}; + +static void handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) { + struct wl_context *context = data; + if (strcmp(interface, wl_compositor_interface.name) == 0) { + context->compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + context->shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + context->seat = wl_registry_bind(registry, name, &wl_seat_interface, 4); + wl_seat_add_listener(context->seat, &seat_listener, data); + } else if (strcmp(interface, wl_data_device_manager_interface.name) == 0) { + context->data_device_manager = wl_registry_bind(registry, name, &wl_data_device_manager_interface, 3); + } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + context->layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1); + } else if (strcmp(interface, wl_output_interface.name) == 0) { + struct wl_output *wl_output = wl_registry_bind(registry, name, &wl_output_interface, 4); + struct output *output = output_create(context, wl_output); + wl_output_set_user_data(wl_output, output); + wl_output_add_listener(wl_output, &output_listener, output); + context_add_output(context, output); + } else if (strcmp(interface, xdg_activation_v1_interface.name) == 0) { + context->activation = wl_registry_bind(registry, name, &xdg_activation_v1_interface, 1); + } +} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = noop, +}; + +// Connect to the Wayland display and run the menu. +int menu_run(struct menu *menu) { + struct wl_context *context = calloc(1, sizeof(struct wl_context)); + context->menu = menu; + menu->context = context; + + context->display = wl_display_connect(NULL); + if (!context->display) { + fprintf(stderr, "Failed to connect to display.\n"); + exit(EXIT_FAILURE); + } + + struct wl_registry *registry = wl_display_get_registry(context->display); + wl_registry_add_listener(registry, &registry_listener, context); + wl_display_roundtrip(context->display); + assert(context->compositor != NULL); + assert(context->shm != NULL); + assert(context->seat != NULL); + assert(context->data_device_manager != NULL); + assert(context->layer_shell != NULL); + assert(context->activation != NULL); + context->registry = registry; + + // Get data device for seat + struct wl_data_device *data_device = wl_data_device_manager_get_data_device( + context->data_device_manager, context->seat); + wl_data_device_add_listener(data_device, &data_device_listener, context); + context->data_device = data_device; + + // Second roundtrip for seat and output listeners + wl_display_roundtrip(context->display); + assert(context->keyboard != NULL); + + if (menu->output_name && !context->output) { + fprintf(stderr, "Output %s not found\n", menu->output_name); + exit(EXIT_FAILURE); + } + + context->surface = wl_compositor_create_surface(context->compositor); + wl_surface_add_listener(context->surface, &surface_listener, context); + + struct zwlr_layer_surface_v1 *layer_surface = zwlr_layer_shell_v1_get_layer_surface( + context->layer_shell, + context->surface, + context->output ? context->output->output : NULL, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "menu" + ); + assert(layer_surface != NULL); + context->layer_surface = layer_surface; + + uint32_t anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT; + if (menu->bottom) { + anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM; + } else { + anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP; + } + + zwlr_layer_surface_v1_set_anchor(layer_surface, anchor); + zwlr_layer_surface_v1_set_size(layer_surface, 0, menu->height); + zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, -1); + zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface, true); + zwlr_layer_surface_v1_add_listener(layer_surface, &layer_surface_listener, context); + + wl_surface_commit(context->surface); + wl_display_roundtrip(context->display); + menu_render_items(menu); + + struct pollfd fds[] = { + { wl_display_get_fd(context->display), POLLIN }, + { context->keyboard->repeat_timer, POLLIN }, + }; + const size_t nfds = sizeof(fds) / sizeof(*fds); + + while (!menu->exit) { + errno = 0; + do { + if (wl_display_flush(context->display) == -1 && errno != EAGAIN) { + fprintf(stderr, "wl_display_flush: %s\n", strerror(errno)); + break; + } + } while (errno == EAGAIN); + + if (poll(fds, nfds, -1) < 0) { + fprintf(stderr, "poll: %s\n", strerror(errno)); + break; + } + + if (fds[0].revents & POLLIN) { + if (wl_display_dispatch(context->display) < 0) { + menu->exit = true; + } + } + + if (fds[1].revents & POLLIN) { + keyboard_repeat(context->keyboard); + } + + // Render the menu if necessary + if (!menu->rendered) { + render_menu(menu); + } + } + + context_destroy(context); + menu->context = NULL; + + if (menu->failure) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} diff --git a/wayland.h b/wayland.h @@ -0,0 +1,19 @@ +#ifndef WMENU_WAYLAND_H +#define WMENU_WAYLAND_H + +#include "menu.h" +#include <wayland-client-protocol.h> + +struct wl_context; + +int menu_run(struct menu *menu); + +int context_get_scale(struct wl_context *context); +struct pool_buffer *context_get_current_buffer(struct wl_context *context); +struct pool_buffer *context_get_next_buffer(struct wl_context *context, int scale); +struct wl_surface *context_get_surface(struct wl_context *context); +struct xkb_state *context_get_xkb_state(struct wl_context *context); +struct xdg_activation_v1 *context_get_xdg_activation(struct wl_context *context); +bool context_paste(struct wl_context *context); + +#endif diff --git a/wmenu-run.c b/wmenu-run.c @@ -0,0 +1,78 @@ +#define _POSIX_C_SOURCE 200809L +#include <dirent.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "menu.h" +#include "wayland.h" +#include "xdg-activation-v1-client-protocol.h" + +static void read_items(struct menu *menu) { + char *path = strdup(getenv("PATH")); + for (char *p = strtok(path, ":"); p != NULL; p = strtok(NULL, ":")) { + DIR *dir = opendir(p); + if (dir == NULL) { + continue; + } + for (struct dirent *ent = readdir(dir); ent != NULL; ent = readdir(dir)) { + if (ent->d_name[0] == '.') { + continue; + } + menu_add_item(menu, strdup(ent->d_name)); + } + closedir(dir); + } + menu_sort_and_deduplicate(menu); + free(path); +} + +struct command { + struct menu *menu; + char *text; + bool exit; +}; + +static void activation_token_done(void *data, struct xdg_activation_token_v1 *activation_token, + const char *token) { + struct command *cmd = data; + xdg_activation_token_v1_destroy(activation_token); + + 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_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, cmd); + xdg_activation_token_v1_commit(activation_token); +} + +int main(int argc, char *argv[]) { + struct menu *menu = menu_create(exec_item); + menu_getopts(menu, argc, argv); + read_items(menu); + int status = menu_run(menu); + menu_destroy(menu); + return status; +} diff --git a/wmenu.c b/wmenu.c @@ -0,0 +1,35 @@ +#define _POSIX_C_SOURCE 200809L + +#include <stdio.h> +#include <string.h> + +#include "menu.h" +#include "wayland.h" + +static void read_items(struct menu *menu) { + char buf[sizeof menu->input]; + while (fgets(buf, sizeof buf, stdin)) { + char *p = strchr(buf, '\n'); + if (p) { + *p = '\0'; + } + 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(print_item); + menu_getopts(menu, argc, argv); + read_items(menu); + int status = menu_run(menu); + menu_destroy(menu); + return status; +}