Move menu and rendering logic into separate files - wmenu - 🔧 fork of wmenu
 (HTM) git clone git@git.drkhsh.at/wmenu.git
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit e8782db9c840e42827dafa4ec1ca9849f91b9b59
 (DIR) parent 1104e8e51b366e7804b94365f8d6a84752693538
 (HTM) Author: adnano <me@adnano.co>
       Date:   Tue, 27 Feb 2024 11:23:12 -0500
       
       Move menu and rendering logic into separate files
       
       Diffstat:
         M main.c                              |    1028 +++----------------------------
         A menu.c                              |     631 +++++++++++++++++++++++++++++++
         A menu.h                              |     102 +++++++++++++++++++++++++++++++
         M meson.build                         |       2 ++
         M pango.c                             |       4 +++-
         M pango.h                             |       4 ++--
         M pool-buffer.c                       |       1 +
         M pool-buffer.h                       |       5 +++++
         A render.c                            |     199 +++++++++++++++++++++++++++++++
         A render.h                            |       9 +++++++++
       
       10 files changed, 1023 insertions(+), 962 deletions(-)
       ---
 (DIR) diff --git a/main.c b/main.c
       @@ -1,7 +1,5 @@
        #define _POSIX_C_SOURCE 200809L
        #include <assert.h>
       -#include <ctype.h>
       -#include <cairo/cairo.h>
        #include <errno.h>
        #include <poll.h>
        #include <stdbool.h>
       @@ -18,482 +16,10 @@
        #include <wayland-client-protocol.h>
        #include <xkbcommon/xkbcommon.h>
        
       -#include "pango.h"
       -#include "pool-buffer.h"
       +#include "menu.h"
       +#include "render.h"
        #include "wlr-layer-shell-unstable-v1-client-protocol.h"
        
       -// 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
       -};
       -
       -// 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
       -};
       -
       -struct output {
       -        struct menu *menu;
       -        struct wl_output *output;
       -        int32_t scale;
       -};
       -
       -struct menu {
       -        struct output *output;
       -        char *output_name;
       -
       -        struct wl_display *display;
       -        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 wl_surface *surface;
       -        struct zwlr_layer_surface_v1 *layer_surface;
       -
       -        struct xkb_context *xkb_context;
       -        struct xkb_keymap *xkb_keymap;
       -        struct xkb_state *xkb_state;
       -
       -        struct wl_data_offer *offer;
       -
       -        struct pool_buffer buffers[2];
       -        struct pool_buffer *current;
       -
       -        int width;
       -        int height;
       -        int line_height;
       -        int padding;
       -        int inputw;
       -        int promptw;
       -        int left_arrow, right_arrow;
       -
       -        bool bottom;
       -        int (*strncmp)(const char *, const char *, size_t);
       -        char *font;
       -        bool vertical;
       -        int lines;
       -        char *prompt;
       -        uint32_t background, foreground;
       -        uint32_t promptbg, promptfg;
       -        uint32_t selectionbg, selectionfg;
       -
       -        char input[BUFSIZ];
       -        size_t cursor;
       -
       -        int repeat_timer;
       -        int repeat_delay;
       -        int repeat_period;
       -        enum wl_keyboard_key_state repeat_key_state;
       -        xkb_keysym_t repeat_sym;
       -
       -        bool run;
       -        bool failure;
       -
       -        struct item *items;       // list of all items
       -        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
       -};
       -
       -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->vertical) {
       -                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;
       -                        while (item) {
       -                                total_width += item->width + 2 * menu->padding;
       -                                if (total_width > max_width) {
       -                                        break;
       -                                }
       -
       -                                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_item(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 tok_len;
       -        menu->matches = NULL;
       -        menu->matches_end = NULL;
       -        menu->sel = NULL;
       -
       -        size_t text_len = strlen(menu->input);
       -
       -        /* tokenize text 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;
       -
       -        struct item *item;
       -        for (item = menu->items; item; item = item->next) {
       -                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, text_len + 1)) {
       -                        append_item(item, &lexact, &exactend);
       -                } else if (!menu->strncmp(tokv[0], item->text, tok_len)) {
       -                        append_item(item, &lprefix, &prefixend);
       -                } else {
       -                        append_item(item, &lsubstr, &substrend);
       -                }
       -        }
       -
       -        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;
       -        }
       -}
       -
       -static void insert(struct menu *menu, const char *s, ssize_t n) {
       -        if (strlen(menu->input) + n > sizeof menu->input - 1) {
       -                return;
       -        }
       -        memmove(menu->input + menu->cursor + n, menu->input + menu->cursor,
       -                        sizeof menu->input - menu->cursor - MAX(n, 0));
       -        if (n > 0 && s != NULL) {
       -                memcpy(menu->input + menu->cursor, s, n);
       -        }
       -        menu->cursor += n;
       -}
       -
       -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) {
       -        size_t len = strlen(menu->input);
       -        while (menu->cursor > 0 && menu->cursor < len && menu->input[nextrune(menu, dir)] == ' ') {
       -                menu->cursor = nextrune(menu, dir);
       -        }
       -        while (menu->cursor > 0 && menu->cursor < len && menu->input[nextrune(menu, dir)] != ' ') {
       -                menu->cursor = nextrune(menu, dir);
       -        }
       -}
       -
       -// Calculate text widths.
       -static void calc_widths(struct menu *menu) {
       -        cairo_t *cairo = menu->current->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 (struct item *item = menu->items; item; item = item->next) {
       -                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) {
       -        render_text(menu, cairo, menu->input, menu->promptw, 0, 0,
       -                0, menu->foreground, menu->padding, menu->padding);
       -}
       -
       -// 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->background;
       -        uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->foreground;
       -
       -        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->background;
       -        uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->foreground;
       -
       -        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_menu(struct menu *menu, cairo_t *cairo) {
       -        // Render background
       -        cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
       -        cairo_set_source_u32(cairo, menu->background);
       -        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->vertical) {
       -                render_vertical_page(menu, cairo, menu->sel->page);
       -        } else {
       -                render_horizontal_page(menu, cairo, menu->sel->page);
       -        }
       -}
       -
       -static void render_frame(struct menu *menu) {
       -        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_menu(menu, cairo);
       -
       -        int scale = menu->output ? menu->output->scale : 1;
       -        menu->current = get_next_buffer(menu->shm,
       -                menu->buffers, menu->width, menu->height, scale);
       -        if (!menu->current) {
       -                goto cleanup;
       -        }
       -
       -        cairo_t *shm = menu->current->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);
       -
       -        wl_surface_set_buffer_scale(menu->surface, scale);
       -        wl_surface_attach(menu->surface, menu->current->buffer, 0, 0);
       -        wl_surface_damage(menu->surface, 0, 0, menu->width, menu->height);
       -        wl_surface_commit(menu->surface);
       -
       -cleanup:
       -        cairo_destroy(cairo);
       -}
       -
        static void noop() {
                // Do nothing
        }
       @@ -521,7 +47,7 @@ static void layer_surface_configure(void *data,
        static void layer_surface_closed(void *data,
                        struct zwlr_layer_surface_v1 *surface) {
                struct menu *menu = data;
       -        menu->run = false;
       +        menu->exit = true;
        }
        
        static const struct zwlr_layer_surface_v1_listener layer_surface_listener = {
       @@ -554,359 +80,68 @@ static const struct wl_output_listener output_listener = {
        
        static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard,
                        uint32_t format, int32_t fd, uint32_t size) {
       -        struct menu *menu = data;
       -        if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) {
       -                close(fd);
       -                menu->run = false;
       -                menu->failure = true;
       -                return;
       -        }
       +        assert(format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1);
       +        struct keyboard *keyboard = data;
       +
       +        // TODO: MAP_PRIVATE vs MAP_SHARED
                char *map_shm = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0);
       -        if (map_shm == MAP_FAILED) {
       -                close(fd);
       -                menu->run = false;
       -                menu->failure = true;
       -                return;
       -        }
       -        menu->xkb_keymap = xkb_keymap_new_from_string(menu->xkb_context,
       +        assert (map_shm != MAP_FAILED);
       +
       +        struct xkb_keymap *keymap = xkb_keymap_new_from_string(keyboard->xkb_context,
                        map_shm, XKB_KEYMAP_FORMAT_TEXT_V1, 0);
                munmap(map_shm, size);
                close(fd);
       -        menu->xkb_state = xkb_state_new(menu->xkb_keymap);
       -}
       -
       -static void keypress(struct menu *menu, enum wl_keyboard_key_state key_state,
       -                xkb_keysym_t sym) {
       -        if (key_state != WL_KEYBOARD_KEY_STATE_PRESSED) {
       -                return;
       -        }
       -
       -        bool ctrl = xkb_state_mod_name_is_active(menu->xkb_state,
       -                        XKB_MOD_NAME_CTRL,
       -                        XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
       -        bool meta = xkb_state_mod_name_is_active(menu->xkb_state,
       -                        XKB_MOD_NAME_ALT,
       -                        XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
       -        bool shift = xkb_state_mod_name_is_active(menu->xkb_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);
       -                        render_frame(menu);
       -                        return;
       -                case XKB_KEY_u:
       -                        // Delete left
       -                        insert(menu, NULL, 0 - menu->cursor);
       -                        match_items(menu);
       -                        render_frame(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);
       -                        render_frame(menu);
       -                        return;
       -                case XKB_KEY_Y:
       -                        // Paste clipboard
       -                        if (!menu->offer) {
       -                                return;
       -                        }
       -
       -                        int fds[2];
       -                        if (pipe(fds) == -1) {
       -                                // Pipe failed
       -                                return;
       -                        }
       -                        wl_data_offer_receive(menu->offer, "text/plain", fds[1]);
       -                        close(fds[1]);
       -
       -                        wl_display_roundtrip(menu->display);
       -
       -                        while (true) {
       -                                char buf[1024];
       -                                ssize_t n = read(fds[0], buf, sizeof(buf));
       -                                if (n <= 0) {
       -                                        break;
       -                                }
       -                                insert(menu, buf, n);
       -                        }
       -                        close(fds[0]);
       -
       -                        wl_data_offer_destroy(menu->offer);
       -                        menu->offer = NULL;
       -                        match_items(menu);
       -                        render_frame(menu);
       -                        return;
       -                case XKB_KEY_Left:
       -                case XKB_KEY_KP_Left:
       -                        movewordedge(menu, -1);
       -                        render_frame(menu);
       -                        return;
       -                case XKB_KEY_Right:
       -                case XKB_KEY_KP_Right:
       -                        movewordedge(menu, +1);
       -                        render_frame(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);
       -                        render_frame(menu);
       -                        return;
       -                case XKB_KEY_f:
       -                        movewordedge(menu, +1);
       -                        render_frame(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) {
       -                        puts(menu->input);
       -                        fflush(stdout);
       -                        menu->run = false;
       -                } else {
       -                        char *text = menu->sel ? menu->sel->text : menu->input;
       -                        puts(text);
       -                        fflush(stdout);
       -                        if (!ctrl) {
       -                                menu->run = false;
       -                        }
       -                }
       -                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;
       -                        render_frame(menu);
       -                } else if (menu->cursor > 0) {
       -                        menu->cursor = nextrune(menu, -1);
       -                        render_frame(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);
       -                        render_frame(menu);
       -                } else if (menu->sel && menu->sel->next_match) {
       -                        menu->sel = menu->sel->next_match;
       -                        render_frame(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_frame(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_frame(menu);
       -                }
       -                break;
       -        case XKB_KEY_Home:
       -        case XKB_KEY_KP_Home:
       -                if (menu->sel == menu->matches) {
       -                        menu->cursor = 0;
       -                        render_frame(menu);
       -                } else {
       -                        menu->sel = menu->matches;
       -                        render_frame(menu);
       -                }
       -                break;
       -        case XKB_KEY_End:
       -        case XKB_KEY_KP_End:
       -                if (menu->cursor < len) {
       -                        menu->cursor = len;
       -                        render_frame(menu);
       -                } else {
       -                        menu->sel = menu->matches_end;
       -                        render_frame(menu);
       -                }
       -                break;
       -        case XKB_KEY_BackSpace:
       -                if (menu->cursor > 0) {
       -                        insert(menu, NULL, nextrune(menu, -1) - menu->cursor);
       -                        match_items(menu);
       -                        render_frame(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);
       -                render_frame(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);
       -                render_frame(menu);
       -                break;
       -        case XKB_KEY_Escape:
       -                menu->failure = true;
       -                menu->run = false;
       -                break;
       -        default:
       -                if (xkb_keysym_to_utf8(sym, buf, 8)) {
       -                        insert(menu, buf, strnlen(buf, 8));
       -                        match_items(menu);
       -                        render_frame(menu);
       -                }
       -        }
       +        keyboard->xkb_state = xkb_state_new(keymap);
        }
        
       -static void keyboard_repeat(struct menu *menu) {
       -        keypress(menu, menu->repeat_key_state, menu->repeat_sym);
       +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 = menu->repeat_period / 1000;
       -        spec.it_value.tv_nsec = (menu->repeat_period % 1000) * 1000000l;
       -        timerfd_settime(menu->repeat_timer, 0, &spec, NULL);
       +        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 menu *menu = data;
       +        struct keyboard *keyboard = data;
        
                enum wl_keyboard_key_state key_state = _key_state;
       -        xkb_keysym_t sym = xkb_state_key_get_one_sym(menu->xkb_state, key + 8);
       -        keypress(menu, key_state, sym);
       +        xkb_keysym_t sym = xkb_state_key_get_one_sym(keyboard->xkb_state, key + 8);
       +        menu_keypress(keyboard->menu, key_state, sym);
        
       -        if (key_state == WL_KEYBOARD_KEY_STATE_PRESSED && menu->repeat_period >= 0) {
       -                menu->repeat_key_state = key_state;
       -                menu->repeat_sym = 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 = menu->repeat_delay / 1000;
       -                spec.it_value.tv_nsec = (menu->repeat_delay % 1000) * 1000000l;
       -                timerfd_settime(menu->repeat_timer, 0, &spec, NULL);
       +                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(menu->repeat_timer, 0, &spec, NULL);
       +                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 menu *menu = data;
       -        menu->repeat_delay = delay;
       +        struct keyboard *keyboard = data;
       +        keyboard->repeat_delay = delay;
                if (rate > 0) {
       -                menu->repeat_period = 1000 / rate;
       +                keyboard->repeat_period = 1000 / rate;
                } else {
       -                menu->repeat_period = -1;
       +                keyboard->repeat_period = -1;
                }
        }
        
       -static void keyboard_modifiers(void *data, struct wl_keyboard *keyboard,
       +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 menu *menu = data;
       -        xkb_state_update_mask(menu->xkb_state, mods_depressed, mods_latched,
       +        struct keyboard *keyboard = data;
       +        xkb_state_update_mask(keyboard->xkb_state, mods_depressed, mods_latched,
                                mods_locked, 0, 0, group);
        }
        
       @@ -924,7 +159,7 @@ static void seat_capabilities(void *data, struct wl_seat *seat,
                struct menu *menu = data;
                if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
                        struct wl_keyboard *keyboard = wl_seat_get_keyboard(seat);
       -                wl_keyboard_add_listener(keyboard, &keyboard_listener, menu);
       +                wl_keyboard_add_listener(keyboard, &keyboard_listener, menu->keyboard);
                }
        }
        
       @@ -981,48 +216,21 @@ static const struct wl_registry_listener registry_listener = {
                .global_remove = noop,
        };
        
       -static void read_stdin(struct menu *menu) {
       -        char buf[sizeof menu->input], *p;
       -        struct item *item, **end;
       -
       -        for(end = &menu->items; fgets(buf, sizeof buf, stdin); *end = item, end = &item->next) {
       -                if((p = strchr(buf, '\n'))) {
       -                        *p = '\0';
       -                }
       -                item = malloc(sizeof *item);
       -                if (!item) {
       -                        return;
       -                }
       -
       -                item->text = strdup(buf);
       -                item->next = item->prev_match = item->next_match = NULL;
       -        }
       +static void keyboard_init(struct keyboard *keyboard, struct menu *menu) {
       +        keyboard->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
       +        assert(keyboard->xkb_context != NULL);
       +        keyboard->repeat_timer = timerfd_create(CLOCK_MONOTONIC, 0);
       +        assert(keyboard->repeat_timer >= 0);
       +        keyboard->menu = menu;
        }
        
       -static void menu_init(struct menu *menu) {
       -        int height = get_font_height(menu->font);
       -        menu->line_height = height + 3;
       -        menu->height = menu->line_height;
       -        if (menu->vertical) {
       -                menu->height += menu->height * menu->lines;
       -        }
       -        menu->padding = height / 2;
       -
       +static void create_surface(struct menu *menu) {
                menu->display = wl_display_connect(NULL);
                if (!menu->display) {
       -                fprintf(stderr, "wl_display_connect: %s\n", strerror(errno));
       +                fprintf(stderr, "Failed to connect to display.\n");
                        exit(EXIT_FAILURE);
                }
        
       -        menu->xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
       -        if (!menu->xkb_context) {
       -                fprintf(stderr, "xkb_context_new: %s\n", strerror(errno));
       -                exit(EXIT_FAILURE);
       -        }
       -
       -        menu->repeat_timer = timerfd_create(CLOCK_MONOTONIC, 0);
       -        assert(menu->repeat_timer >= 0);
       -
                struct wl_registry *registry = wl_display_get_registry(menu->display);
                wl_registry_add_listener(registry, &registry_listener, menu);
                wl_display_roundtrip(menu->display);
       @@ -1044,19 +252,18 @@ static void menu_init(struct menu *menu) {
                        fprintf(stderr, "Output %s not found\n", menu->output_name);
                        exit(EXIT_FAILURE);
                }
       -}
        
       -static void menu_create_surface(struct menu *menu) {
                menu->surface = wl_compositor_create_surface(menu->compositor);
                wl_surface_add_listener(menu->surface, &surface_listener, menu);
       -        menu->layer_surface = zwlr_layer_shell_v1_get_layer_surface(
       +
       +        struct zwlr_layer_surface_v1 *layer_surface = zwlr_layer_shell_v1_get_layer_surface(
                        menu->layer_shell,
                        menu->surface,
                        NULL,
                        ZWLR_LAYER_SHELL_V1_LAYER_TOP,
                        "menu"
                );
       -        assert(menu->layer_surface != NULL);
       +        assert(layer_surface != NULL);
        
                uint32_t anchor = ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT |
                        ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT;
       @@ -1066,137 +273,40 @@ static void menu_create_surface(struct menu *menu) {
                        anchor |= ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP;
                }
        
       -        zwlr_layer_surface_v1_set_anchor(menu->layer_surface, anchor);
       -        zwlr_layer_surface_v1_set_size(menu->layer_surface, 0, menu->height);
       -        zwlr_layer_surface_v1_set_exclusive_zone(menu->layer_surface, -1);
       -        zwlr_layer_surface_v1_set_keyboard_interactivity(menu->layer_surface, true);
       -        zwlr_layer_surface_v1_add_listener(menu->layer_surface,
       -                &layer_surface_listener, menu);
       +        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, menu);
        
                wl_surface_commit(menu->surface);
                wl_display_roundtrip(menu->display);
        }
        
       -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;
       -}
       -
       -int main(int argc, char **argv) {
       -        struct menu menu = {
       -                .strncmp = strncmp,
       -                .font = "monospace 10",
       -                .vertical = false,
       -                .background = 0x222222ff,
       -                .foreground = 0xbbbbbbff,
       -                .promptbg = 0x005577ff,
       -                .promptfg = 0xeeeeeeff,
       -                .selectionbg = 0x005577ff,
       -                .selectionfg = 0xeeeeeeff,
       -                .run = true,
       -        };
       -
       -        const char *usage =
       -                "Usage: wmenu [-biv] [-f font] [-l lines] [-o output] [-p prompt]\n"
       -                "\t[-N color] [-n color] [-M color] [-m color] [-S color] [-s color]\n";
       +int main(int argc, char *argv[]) {
       +        struct menu *menu = calloc(1, sizeof(struct menu));
       +        menu_init(menu, argc, argv);
        
       -        int opt;
       -        while ((opt = getopt(argc, argv, "bhivf: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 'v':
       -                        puts("wmenu " VERSION);
       -                        exit(EXIT_SUCCESS);
       -                case 'f':
       -                        menu.font = optarg;
       -                        break;
       -                case 'l':
       -                        menu.vertical = true;
       -                        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.background)) {
       -                                fprintf(stderr, "Invalid background color: %s", optarg);
       -                        }
       -                        break;
       -                case 'n':
       -                        if (!parse_color(optarg, &menu.foreground)) {
       -                                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);
       -        }
       +        struct keyboard *keyboard = calloc(1, sizeof(struct keyboard));
       +        keyboard_init(keyboard, menu);
       +        menu->keyboard = keyboard;
        
       -        menu_init(&menu);
       -        menu_create_surface(&menu);
       -        render_frame(&menu);
       +        create_surface(menu);
       +        render_menu(menu);
        
       -        read_stdin(&menu);
       -        calc_widths(&menu);
       -        match_items(&menu);
       -        render_frame(&menu);
       +        read_menu_items(menu);
       +        render_menu(menu);
        
                struct pollfd fds[] = {
       -                { wl_display_get_fd(menu.display), POLLIN },
       -                { menu.repeat_timer, POLLIN },
       +                { wl_display_get_fd(menu->display), POLLIN },
       +                { keyboard->repeat_timer, POLLIN },
                };
                const size_t nfds = sizeof(fds) / sizeof(*fds);
        
       -        while (menu.run) {
       +        while (!menu->exit) {
                        errno = 0;
                        do {
       -                        if (wl_display_flush(menu.display) == -1 && errno != EAGAIN) {
       +                        if (wl_display_flush(menu->display) == -1 && errno != EAGAIN) {
                                        fprintf(stderr, "wl_display_flush: %s\n", strerror(errno));
                                        break;
                                }
       @@ -1208,19 +318,19 @@ int main(int argc, char **argv) {
                        }
        
                        if (fds[0].revents & POLLIN) {
       -                        if (wl_display_dispatch(menu.display) < 0) {
       -                                menu.run = false;
       +                        if (wl_display_dispatch(menu->display) < 0) {
       +                                menu->exit = true;
                                }
                        }
        
                        if (fds[1].revents & POLLIN) {
       -                        keyboard_repeat(&menu);
       +                        keyboard_repeat(keyboard);
                        }
                }
        
       -        wl_display_disconnect(menu.display);
       +        wl_display_disconnect(menu->display);
        
       -        if (menu.failure) {
       +        if (menu->failure) {
                        return EXIT_FAILURE;
                }
                return EXIT_SUCCESS;
 (DIR) diff --git a/menu.c b/menu.c
       @@ -0,0 +1,631 @@
       +#define _POSIX_C_SOURCE 200809L
       +#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"
       +
       +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;
       +}
       +
       +// Initialize the menu.
       +void menu_init(struct menu *menu, int argc, char *argv[]) {
       +        menu->strncmp = strncmp;
       +        menu->font = "monospace 10";
       +        menu->background = 0x222222ff;
       +        menu->foreground = 0xbbbbbbff;
       +        menu->promptbg = 0x005577ff;
       +        menu->promptfg = 0xeeeeeeff;
       +        menu->selectionbg = 0x005577ff;
       +        menu->selectionfg = 0xeeeeeeff;
       +
       +        const char *usage =
       +                "Usage: wmenu [-biv] [-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) {
       +                switch (opt) {
       +                case 'b':
       +                        menu->bottom = true;
       +                        break;
       +                case 'i':
       +                        menu->strncmp = strncasecmp;
       +                        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->background)) {
       +                                fprintf(stderr, "Invalid background color: %s", optarg);
       +                        }
       +                        break;
       +                case 'n':
       +                        if (!parse_color(optarg, &menu->foreground)) {
       +                                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 + 3;
       +        menu->height = menu->line_height;
       +        if (menu->lines > 0) {
       +                menu->height += menu->height * menu->lines;
       +        }
       +        menu->padding = height / 2;
       +}
       +
       +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;
       +                        while (item) {
       +                                total_width += item->width + 2 * menu->padding;
       +                                if (total_width > max_width) {
       +                                        break;
       +                                }
       +
       +                                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_item(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 tok_len;
       +        menu->matches = NULL;
       +        menu->matches_end = NULL;
       +        menu->sel = NULL;
       +
       +        size_t text_len = strlen(menu->input);
       +
       +        /* tokenize text 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;
       +
       +        struct item *item;
       +        for (item = menu->items; item; item = item->next) {
       +                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, text_len + 1)) {
       +                        append_item(item, &lexact, &exactend);
       +                } else if (!menu->strncmp(tokv[0], item->text, tok_len)) {
       +                        append_item(item, &lprefix, &prefixend);
       +                } else {
       +                        append_item(item, &lsubstr, &substrend);
       +                }
       +        }
       +
       +        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;
       +        }
       +}
       +
       +// Read menu items from standard input.
       +void read_menu_items(struct menu *menu) {
       +        char buf[sizeof menu->input], *p;
       +        struct item *item, **end;
       +
       +        for(end = &menu->items; fgets(buf, sizeof buf, stdin); *end = item, end = &item->next) {
       +                if((p = strchr(buf, '\n'))) {
       +                        *p = '\0';
       +                }
       +                item = malloc(sizeof *item);
       +                if (!item) {
       +                        return;
       +                }
       +
       +                item->text = strdup(buf);
       +                item->next = item->prev_match = item->next_match = NULL;
       +        }
       +
       +        calc_widths(menu);
       +        match_items(menu);
       +}
       +
       +static void insert(struct menu *menu, const char *s, ssize_t n) {
       +        if (strlen(menu->input) + n > sizeof menu->input - 1) {
       +                return;
       +        }
       +        memmove(menu->input + menu->cursor + n, menu->input + menu->cursor,
       +                        sizeof menu->input - menu->cursor - MAX(n, 0));
       +        if (n > 0 && s != NULL) {
       +                memcpy(menu->input + menu->cursor, s, n);
       +        }
       +        menu->cursor += n;
       +}
       +
       +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) {
       +        size_t len = strlen(menu->input);
       +        while (menu->cursor > 0 && menu->cursor < len && menu->input[nextrune(menu, dir)] == ' ') {
       +                menu->cursor = nextrune(menu, dir);
       +        }
       +        while (menu->cursor > 0 && menu->cursor < len && menu->input[nextrune(menu, dir)] != ' ') {
       +                menu->cursor = nextrune(menu, dir);
       +        }
       +}
       +
       +// 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;
       +        }
       +
       +        bool ctrl = xkb_state_mod_name_is_active(menu->keyboard->xkb_state,
       +                        XKB_MOD_NAME_CTRL,
       +                        XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
       +        bool meta = xkb_state_mod_name_is_active(menu->keyboard->xkb_state,
       +                        XKB_MOD_NAME_ALT,
       +                        XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
       +        bool shift = xkb_state_mod_name_is_active(menu->keyboard->xkb_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);
       +                        render_menu(menu);
       +                        return;
       +                case XKB_KEY_u:
       +                        // Delete left
       +                        insert(menu, NULL, 0 - menu->cursor);
       +                        match_items(menu);
       +                        render_menu(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);
       +                        render_menu(menu);
       +                        return;
       +                case XKB_KEY_Y:
       +                        // Paste clipboard
       +                        if (!menu->offer) {
       +                                return;
       +                        }
       +
       +                        int fds[2];
       +                        if (pipe(fds) == -1) {
       +                                // Pipe failed
       +                                return;
       +                        }
       +                        wl_data_offer_receive(menu->offer, "text/plain", fds[1]);
       +                        close(fds[1]);
       +
       +                        wl_display_roundtrip(menu->display);
       +
       +                        while (true) {
       +                                char buf[1024];
       +                                ssize_t n = read(fds[0], buf, sizeof(buf));
       +                                if (n <= 0) {
       +                                        break;
       +                                }
       +                                insert(menu, buf, n);
       +                        }
       +                        close(fds[0]);
       +
       +                        wl_data_offer_destroy(menu->offer);
       +                        menu->offer = NULL;
       +                        match_items(menu);
       +                        render_menu(menu);
       +                        return;
       +                case XKB_KEY_Left:
       +                case XKB_KEY_KP_Left:
       +                        movewordedge(menu, -1);
       +                        render_menu(menu);
       +                        return;
       +                case XKB_KEY_Right:
       +                case XKB_KEY_KP_Right:
       +                        movewordedge(menu, +1);
       +                        render_menu(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);
       +                        render_menu(menu);
       +                        return;
       +                case XKB_KEY_f:
       +                        movewordedge(menu, +1);
       +                        render_menu(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) {
       +                        puts(menu->input);
       +                        fflush(stdout);
       +                        menu->exit = true;
       +                } else {
       +                        char *text = menu->sel ? menu->sel->text : menu->input;
       +                        puts(text);
       +                        fflush(stdout);
       +                        if (!ctrl) {
       +                                menu->exit = true;
       +                        }
       +                }
       +                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;
       +                        render_menu(menu);
       +                } else if (menu->cursor > 0) {
       +                        menu->cursor = nextrune(menu, -1);
       +                        render_menu(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);
       +                        render_menu(menu);
       +                } else if (menu->sel && menu->sel->next_match) {
       +                        menu->sel = menu->sel->next_match;
       +                        render_menu(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);
       +                }
       +                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);
       +                }
       +                break;
       +        case XKB_KEY_Home:
       +        case XKB_KEY_KP_Home:
       +                if (menu->sel == menu->matches) {
       +                        menu->cursor = 0;
       +                        render_menu(menu);
       +                } else {
       +                        menu->sel = menu->matches;
       +                        render_menu(menu);
       +                }
       +                break;
       +        case XKB_KEY_End:
       +        case XKB_KEY_KP_End:
       +                if (menu->cursor < len) {
       +                        menu->cursor = len;
       +                        render_menu(menu);
       +                } else {
       +                        menu->sel = menu->matches_end;
       +                        render_menu(menu);
       +                }
       +                break;
       +        case XKB_KEY_BackSpace:
       +                if (menu->cursor > 0) {
       +                        insert(menu, NULL, nextrune(menu, -1) - menu->cursor);
       +                        match_items(menu);
       +                        render_menu(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);
       +                render_menu(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);
       +                render_menu(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);
       +                        render_menu(menu);
       +                }
       +        }
       +}
 (DIR) diff --git a/menu.h b/menu.h
       @@ -0,0 +1,102 @@
       +#ifndef WMENU_MENU_H
       +#define WMENU_MENU_H
       +
       +#include <xkbcommon/xkbcommon.h>
       +
       +#include "pool-buffer.h"
       +
       +// 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
       +};
       +
       +// 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
       +};
       +
       +// A Wayland output.
       +struct output {
       +        struct menu *menu;
       +        struct wl_output *output;
       +        int32_t scale;
       +};
       +
       +// Keyboard state.
       +struct keyboard {
       +        struct menu *menu;
       +
       +        struct xkb_context *xkb_context;
       +        struct xkb_state *xkb_state;
       +
       +        int repeat_timer;
       +        int repeat_delay;
       +        int repeat_period;
       +        enum wl_keyboard_key_state repeat_key_state;
       +        xkb_keysym_t repeat_sym;
       +};
       +
       +// Menu state.
       +struct menu {
       +        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 wl_display *display;
       +        struct wl_surface *surface;
       +        struct wl_data_offer *offer;
       +
       +        struct keyboard *keyboard;
       +        struct output *output;
       +        char *output_name;
       +
       +        struct pool_buffer buffers[2];
       +        struct pool_buffer *current;
       +
       +        int width;
       +        int height;
       +        int line_height;
       +        int padding;
       +        int inputw;
       +        int promptw;
       +        int left_arrow;
       +        int right_arrow;
       +
       +        bool bottom;
       +        int (*strncmp)(const char *, const char *, size_t);
       +        char *font;
       +        int lines;
       +        char *prompt;
       +        uint32_t background, foreground;
       +        uint32_t promptbg, promptfg;
       +        uint32_t selectionbg, selectionfg;
       +
       +        char input[BUFSIZ];
       +        size_t cursor;
       +
       +        struct item *items;       // list of all items
       +        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
       +
       +        bool exit;
       +        bool failure;
       +};
       +
       +void menu_init(struct menu *menu, int argc, char *argv[]);
       +void read_menu_items(struct menu *menu);
       +void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state,
       +                xkb_keysym_t sym);
       +
       +#endif
 (DIR) diff --git a/meson.build b/meson.build
       @@ -37,8 +37,10 @@ executable(
                'wmenu',
                files(
                        'main.c',
       +                'menu.c',
                        'pango.c',
                        'pool-buffer.c',
       +                'render.c',
                ),
                dependencies: [
                        cairo,
 (DIR) diff --git a/pango.c b/pango.c
       @@ -5,7 +5,9 @@
        #include <stdlib.h>
        #include <string.h>
        
       -int get_font_height(char *fontstr) {
       +#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);
 (DIR) diff --git a/pango.h b/pango.h
       @@ -1,5 +1,5 @@
       -#ifndef DMENU_PANGO_H
       -#define DMENU_PANGO_H
       +#ifndef WMENU_PANGO_H
       +#define WMENU_PANGO_H
        #include <stdbool.h>
        #include <cairo/cairo.h>
        #include <pango/pangocairo.h>
 (DIR) diff --git a/pool-buffer.c b/pool-buffer.c
       @@ -11,6 +11,7 @@
        #include <time.h>
        #include <unistd.h>
        #include <wayland-client.h>
       +
        #include "pool-buffer.h"
        
        static void randname(char *buf) {
 (DIR) diff --git a/pool-buffer.h b/pool-buffer.h
       @@ -1,4 +1,7 @@
        /* 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>
       @@ -19,3 +22,5 @@ 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);
        void destroy_buffer(struct pool_buffer *buffer);
       +
       +#endif
 (DIR) diff --git a/render.c b/render.c
       @@ -0,0 +1,199 @@
       +#include <cairo/cairo.h>
       +
       +#include "render.h"
       +
       +#include "menu.h"
       +#include "pango.h"
       +
       +// Calculate text widths.
       +void calc_widths(struct menu *menu) {
       +        cairo_t *cairo = menu->current->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 (struct item *item = menu->items; item; item = item->next) {
       +                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) {
       +        render_text(menu, cairo, menu->input, menu->promptw, 0, 0,
       +                0, menu->foreground, menu->padding, menu->padding);
       +}
       +
       +// 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->background;
       +        uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->foreground;
       +
       +        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->background;
       +        uint32_t fg_color = menu->sel == item ? menu->selectionfg : menu->foreground;
       +
       +        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->background);
       +        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) {
       +        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 = menu->output ? menu->output->scale : 1;
       +        menu->current = get_next_buffer(menu->shm,
       +                menu->buffers, menu->width, menu->height, scale);
       +        if (!menu->current) {
       +                goto cleanup;
       +        }
       +
       +        cairo_t *shm = menu->current->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);
       +
       +        wl_surface_set_buffer_scale(menu->surface, scale);
       +        wl_surface_attach(menu->surface, menu->current->buffer, 0, 0);
       +        wl_surface_damage(menu->surface, 0, 0, menu->width, menu->height);
       +        wl_surface_commit(menu->surface);
       +
       +cleanup:
       +        cairo_destroy(cairo);
       +}
 (DIR) 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