
Adds selection wrapping, such that pressing up at the top of the list will select the last item, and pressing down at the bottom of the list will select the first item. Additionally, cursor movement is now exclusively handled by Left/Right and selection movement is exclusively handled by Up/Down, as the new wrapping behavior would prevent cursor movement from ever triggering.
687 lines
15 KiB
C
687 lines
15 KiB
C
#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"
|
|
#include "wayland.h"
|
|
|
|
// Creates and returns a new menu.
|
|
struct menu *menu_create(menu_callback callback) {
|
|
struct menu *menu = calloc(1, sizeof(struct menu));
|
|
menu->strncmp = strncmp;
|
|
menu->font = "monospace 10";
|
|
menu->normalbg = 0x222222ff;
|
|
menu->normalfg = 0xbbbbbbff;
|
|
menu->promptbg = 0x005577ff;
|
|
menu->promptfg = 0xeeeeeeff;
|
|
menu->selectionbg = 0x005577ff;
|
|
menu->selectionfg = 0xeeeeeeff;
|
|
menu->callback = callback;
|
|
return menu;
|
|
}
|
|
|
|
static void free_pages(struct menu *menu) {
|
|
struct page *next = menu->pages;
|
|
while (next) {
|
|
struct page *page = next;
|
|
next = page->next;
|
|
free(page);
|
|
}
|
|
}
|
|
|
|
static void free_item(struct item *item) {
|
|
free(item->text);
|
|
free(item);
|
|
}
|
|
|
|
static void free_items(struct menu *menu) {
|
|
struct item *next = menu->items;
|
|
while (next) {
|
|
struct item *item = next;
|
|
next = item->next;
|
|
free_item(item);
|
|
}
|
|
}
|
|
|
|
// Destroys the menu, freeing memory associated with it.
|
|
void menu_destroy(struct menu *menu) {
|
|
free_pages(menu);
|
|
free_items(menu);
|
|
free(menu);
|
|
}
|
|
|
|
static bool parse_color(const char *color, uint32_t *result) {
|
|
if (color[0] == '#') {
|
|
++color;
|
|
}
|
|
size_t len = strlen(color);
|
|
if ((len != 6 && len != 8) || !isxdigit(color[0]) || !isxdigit(color[1])) {
|
|
return false;
|
|
}
|
|
char *ptr;
|
|
uint32_t parsed = (uint32_t)strtoul(color, &ptr, 16);
|
|
if (*ptr != '\0') {
|
|
return false;
|
|
}
|
|
*result = len == 6 ? ((parsed << 8) | 0xFF) : parsed;
|
|
return true;
|
|
}
|
|
|
|
// Parse menu options from command line arguments.
|
|
void menu_getopts(struct menu *menu, int argc, char *argv[]) {
|
|
const char *usage =
|
|
"Usage: wmenu [-biPv] [-f font] [-l lines] [-o output] [-p prompt]\n"
|
|
"\t[-N color] [-n color] [-M color] [-m color] [-S color] [-s color]\n";
|
|
|
|
int opt;
|
|
while ((opt = getopt(argc, argv, "bhiPvf:l:o:p:N:n:M:m:S:s:")) != -1) {
|
|
switch (opt) {
|
|
case 'b':
|
|
menu->bottom = true;
|
|
break;
|
|
case 'i':
|
|
menu->strncmp = strncasecmp;
|
|
break;
|
|
case 'P':
|
|
menu->passwd = true;
|
|
break;
|
|
case 'v':
|
|
puts("wmenu " VERSION);
|
|
exit(EXIT_SUCCESS);
|
|
case 'f':
|
|
menu->font = optarg;
|
|
break;
|
|
case 'l':
|
|
menu->lines = atoi(optarg);
|
|
break;
|
|
case 'o':
|
|
menu->output_name = optarg;
|
|
break;
|
|
case 'p':
|
|
menu->prompt = optarg;
|
|
break;
|
|
case 'N':
|
|
if (!parse_color(optarg, &menu->normalbg)) {
|
|
fprintf(stderr, "Invalid background color: %s", optarg);
|
|
}
|
|
break;
|
|
case 'n':
|
|
if (!parse_color(optarg, &menu->normalfg)) {
|
|
fprintf(stderr, "Invalid foreground color: %s", optarg);
|
|
}
|
|
break;
|
|
case 'M':
|
|
if (!parse_color(optarg, &menu->promptbg)) {
|
|
fprintf(stderr, "Invalid prompt background color: %s", optarg);
|
|
}
|
|
break;
|
|
case 'm':
|
|
if (!parse_color(optarg, &menu->promptfg)) {
|
|
fprintf(stderr, "Invalid prompt foreground color: %s", optarg);
|
|
}
|
|
break;
|
|
case 'S':
|
|
if (!parse_color(optarg, &menu->selectionbg)) {
|
|
fprintf(stderr, "Invalid selection background color: %s", optarg);
|
|
}
|
|
break;
|
|
case 's':
|
|
if (!parse_color(optarg, &menu->selectionfg)) {
|
|
fprintf(stderr, "Invalid selection foreground color: %s", optarg);
|
|
}
|
|
break;
|
|
default:
|
|
fprintf(stderr, "%s", usage);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
|
|
if (optind < argc) {
|
|
fprintf(stderr, "%s", usage);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
int height = get_font_height(menu->font);
|
|
menu->line_height = height + 2;
|
|
menu->height = menu->line_height;
|
|
if (menu->lines > 0) {
|
|
menu->height += menu->height * menu->lines;
|
|
}
|
|
menu->padding = height / 2;
|
|
}
|
|
|
|
// Add an item to the menu.
|
|
void menu_add_item(struct menu *menu, char *text, bool sort) {
|
|
struct item *new = calloc(1, sizeof(struct item));
|
|
if (!new) {
|
|
return;
|
|
}
|
|
new->text = text;
|
|
|
|
if (sort) {
|
|
for (struct item **item = &menu->items; *item; item = &(*item)->next) {
|
|
int result = strcmp(new->text, (*item)->text);
|
|
if (result == 0) {
|
|
free_item(new);
|
|
return;
|
|
}
|
|
if (result < 0) {
|
|
new->next = *item;
|
|
*item = new;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (menu->lastitem) {
|
|
menu->lastitem->next = new;
|
|
} else {
|
|
menu->items = new;
|
|
}
|
|
menu->lastitem = new;
|
|
}
|
|
|
|
static void append_page(struct page *page, struct page **first, struct page **last) {
|
|
if (*last) {
|
|
(*last)->next = page;
|
|
} else {
|
|
*first = page;
|
|
}
|
|
page->prev = *last;
|
|
page->next = NULL;
|
|
*last = page;
|
|
}
|
|
|
|
static void page_items(struct menu *menu) {
|
|
// Free existing pages
|
|
while (menu->pages != NULL) {
|
|
struct page *page = menu->pages;
|
|
menu->pages = menu->pages->next;
|
|
free(page);
|
|
}
|
|
|
|
if (!menu->matches) {
|
|
return;
|
|
}
|
|
|
|
// Make new pages
|
|
if (menu->lines > 0) {
|
|
struct page *pages_end = NULL;
|
|
struct item *item = menu->matches;
|
|
while (item) {
|
|
struct page *page = calloc(1, sizeof(struct page));
|
|
page->first = item;
|
|
|
|
for (int i = 1; item && i <= menu->lines; i++) {
|
|
item->page = page;
|
|
page->last = item;
|
|
item = item->next_match;
|
|
}
|
|
append_page(page, &menu->pages, &pages_end);
|
|
}
|
|
} else {
|
|
// Calculate available space
|
|
int max_width = menu->width - menu->inputw - menu->promptw
|
|
- menu->left_arrow - menu->right_arrow;
|
|
|
|
struct page *pages_end = NULL;
|
|
struct item *item = menu->matches;
|
|
while (item) {
|
|
struct page *page = calloc(1, sizeof(struct page));
|
|
page->first = item;
|
|
|
|
int total_width = 0;
|
|
int items = 0;
|
|
while (item) {
|
|
total_width += item->width + 2 * menu->padding;
|
|
if (total_width > max_width && items > 0) {
|
|
break;
|
|
}
|
|
items++;
|
|
|
|
item->page = page;
|
|
page->last = item;
|
|
item = item->next_match;
|
|
}
|
|
append_page(page, &menu->pages, &pages_end);
|
|
}
|
|
}
|
|
}
|
|
|
|
static const char *fstrstr(struct menu *menu, const char *s, const char *sub) {
|
|
for (size_t len = strlen(sub); *s; s++) {
|
|
if (!menu->strncmp(s, sub, len)) {
|
|
return s;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void append_match(struct item *item, struct item **first, struct item **last) {
|
|
if (*last) {
|
|
(*last)->next_match = item;
|
|
} else {
|
|
*first = item;
|
|
}
|
|
item->prev_match = *last;
|
|
item->next_match = NULL;
|
|
*last = item;
|
|
}
|
|
|
|
static void match_items(struct menu *menu) {
|
|
struct item *lexact = NULL, *exactend = NULL;
|
|
struct item *lprefix = NULL, *prefixend = NULL;
|
|
struct item *lsubstr = NULL, *substrend = NULL;
|
|
char buf[sizeof menu->input], *tok;
|
|
char **tokv = NULL;
|
|
int i, tokc = 0;
|
|
size_t tok_len;
|
|
menu->matches = NULL;
|
|
menu->matches_end = NULL;
|
|
menu->sel = NULL;
|
|
|
|
size_t input_len = strlen(menu->input);
|
|
|
|
/* tokenize input by space for matching the tokens individually */
|
|
strcpy(buf, menu->input);
|
|
tok = strtok(buf, " ");
|
|
while (tok) {
|
|
tokv = realloc(tokv, (tokc + 1) * sizeof *tokv);
|
|
if (!tokv) {
|
|
fprintf(stderr, "could not realloc %zu bytes",
|
|
(tokc + 1) * sizeof *tokv);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
tokv[tokc] = tok;
|
|
tokc++;
|
|
tok = strtok(NULL, " ");
|
|
}
|
|
tok_len = tokc ? strlen(tokv[0]) : 0;
|
|
|
|
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, input_len + 1)) {
|
|
append_match(item, &lexact, &exactend);
|
|
} else if (!menu->strncmp(tokv[0], item->text, tok_len)) {
|
|
append_match(item, &lprefix, &prefixend);
|
|
} else {
|
|
append_match(item, &lsubstr, &substrend);
|
|
}
|
|
}
|
|
|
|
free(tokv);
|
|
|
|
if (lexact) {
|
|
menu->matches = lexact;
|
|
menu->matches_end = exactend;
|
|
}
|
|
if (lprefix) {
|
|
if (menu->matches_end) {
|
|
menu->matches_end->next_match = lprefix;
|
|
lprefix->prev_match = menu->matches_end;
|
|
} else {
|
|
menu->matches = lprefix;
|
|
}
|
|
menu->matches_end = prefixend;
|
|
}
|
|
if (lsubstr) {
|
|
if (menu->matches_end) {
|
|
menu->matches_end->next_match = lsubstr;
|
|
lsubstr->prev_match = menu->matches_end;
|
|
} else {
|
|
menu->matches = lsubstr;
|
|
}
|
|
menu->matches_end = substrend;
|
|
}
|
|
|
|
page_items(menu);
|
|
if (menu->pages) {
|
|
menu->sel = menu->pages->first;
|
|
}
|
|
}
|
|
|
|
// Render menu items.
|
|
void menu_render_items(struct menu *menu) {
|
|
render_menu(menu);
|
|
calc_widths(menu);
|
|
match_items(menu);
|
|
render_menu(menu);
|
|
}
|
|
|
|
static void insert(struct menu *menu, const char *text, ssize_t len) {
|
|
if (strlen(menu->input) + len > sizeof menu->input - 1) {
|
|
return;
|
|
}
|
|
memmove(menu->input + menu->cursor + len, menu->input + menu->cursor,
|
|
sizeof menu->input - menu->cursor - MAX(len, 0));
|
|
if (len > 0 && text != NULL) {
|
|
memcpy(menu->input + menu->cursor, text, len);
|
|
}
|
|
menu->cursor += len;
|
|
}
|
|
|
|
// Add pasted text to the menu input.
|
|
void menu_paste(struct menu *menu, const char *text, ssize_t len) {
|
|
insert(menu, text, len);
|
|
}
|
|
|
|
static size_t nextrune(struct menu *menu, int incr) {
|
|
size_t n, len;
|
|
|
|
len = strlen(menu->input);
|
|
for(n = menu->cursor + incr; n < len && (menu->input[n] & 0xc0) == 0x80; n += incr);
|
|
return n;
|
|
}
|
|
|
|
// Move the cursor to the beginning or end of the word, skipping over any preceding whitespace.
|
|
static void movewordedge(struct menu *menu, int dir) {
|
|
if (dir < 0) {
|
|
// Move to beginning of word
|
|
while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] == ' ') {
|
|
menu->cursor = nextrune(menu, -1);
|
|
}
|
|
while (menu->cursor > 0 && menu->input[nextrune(menu, -1)] != ' ') {
|
|
menu->cursor = nextrune(menu, -1);
|
|
}
|
|
} else {
|
|
// Move to end of word
|
|
size_t len = strlen(menu->input);
|
|
while (menu->cursor < len && menu->input[menu->cursor] == ' ') {
|
|
menu->cursor = nextrune(menu, +1);
|
|
}
|
|
while (menu->cursor < len && menu->input[menu->cursor] != ' ') {
|
|
menu->cursor = nextrune(menu, +1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle a keypress.
|
|
void menu_keypress(struct menu *menu, enum wl_keyboard_key_state key_state,
|
|
xkb_keysym_t sym) {
|
|
if (key_state != WL_KEYBOARD_KEY_STATE_PRESSED) {
|
|
return;
|
|
}
|
|
|
|
struct xkb_state *state = context_get_xkb_state(menu->context);
|
|
bool ctrl = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_CTRL,
|
|
XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
|
|
bool meta = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_ALT,
|
|
XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
|
|
bool shift = xkb_state_mod_name_is_active(state, XKB_MOD_NAME_SHIFT,
|
|
XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED);
|
|
|
|
size_t len = strlen(menu->input);
|
|
|
|
if (ctrl) {
|
|
// Emacs-style line editing bindings
|
|
switch (sym) {
|
|
case XKB_KEY_a:
|
|
sym = XKB_KEY_Home;
|
|
break;
|
|
case XKB_KEY_b:
|
|
sym = XKB_KEY_Left;
|
|
break;
|
|
case XKB_KEY_c:
|
|
sym = XKB_KEY_Escape;
|
|
break;
|
|
case XKB_KEY_d:
|
|
sym = XKB_KEY_Delete;
|
|
break;
|
|
case XKB_KEY_e:
|
|
sym = XKB_KEY_End;
|
|
break;
|
|
case XKB_KEY_f:
|
|
sym = XKB_KEY_Right;
|
|
break;
|
|
case XKB_KEY_g:
|
|
sym = XKB_KEY_Escape;
|
|
break;
|
|
case XKB_KEY_bracketleft:
|
|
sym = XKB_KEY_Escape;
|
|
break;
|
|
case XKB_KEY_h:
|
|
sym = XKB_KEY_BackSpace;
|
|
break;
|
|
case XKB_KEY_i:
|
|
sym = XKB_KEY_Tab;
|
|
break;
|
|
case XKB_KEY_j:
|
|
case XKB_KEY_J:
|
|
case XKB_KEY_m:
|
|
case XKB_KEY_M:
|
|
sym = XKB_KEY_Return;
|
|
ctrl = false;
|
|
break;
|
|
case XKB_KEY_n:
|
|
sym = XKB_KEY_Down;
|
|
break;
|
|
case XKB_KEY_p:
|
|
sym = XKB_KEY_Up;
|
|
break;
|
|
|
|
case XKB_KEY_k:
|
|
// Delete right
|
|
menu->input[menu->cursor] = '\0';
|
|
match_items(menu);
|
|
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 (!context_paste(menu->context)) {
|
|
return;
|
|
}
|
|
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) {
|
|
menu->callback(menu, menu->input, true);
|
|
} else {
|
|
char *text = menu->sel ? menu->sel->text : menu->input;
|
|
menu->callback(menu, text, !ctrl);
|
|
}
|
|
break;
|
|
case XKB_KEY_Left:
|
|
case XKB_KEY_KP_Left:
|
|
if (menu->cursor > 0) {
|
|
menu->cursor = nextrune(menu, -1);
|
|
render_menu(menu);
|
|
}
|
|
break;
|
|
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->sel && menu->sel == menu->matches) {
|
|
menu->sel = menu->matches_end;
|
|
render_menu(menu);
|
|
}
|
|
break;
|
|
case XKB_KEY_Right:
|
|
case XKB_KEY_KP_Right:
|
|
if (menu->cursor < len) {
|
|
menu->cursor = nextrune(menu, +1);
|
|
render_menu(menu);
|
|
}
|
|
break;
|
|
case XKB_KEY_Down:
|
|
case XKB_KEY_KP_Down:
|
|
if (menu->sel && menu->sel->next_match) {
|
|
menu->sel = menu->sel->next_match;
|
|
render_menu(menu);
|
|
} else if (menu->sel && menu->sel == menu->matches_end) {
|
|
menu->sel = menu->matches;
|
|
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);
|
|
}
|
|
}
|
|
}
|