dmenu-wlroots/dmenu.c

613 lines
17 KiB
C

/* See LICENSE file for copyright and license details. */
#include <ctype.h>
#include <stdbool.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <wayland-client.h>
#include <xkbcommon/xkbcommon.h>
#include "draw.h"
#define INRECT(x,y,rx,ry,rw,rh) ((x) >= (rx) && (x) < (rx)+(rw) && (y) >= (ry) && (y) < (ry)+(rh))
#define MIN(a,b) ((a) < (b) ? (a) : (b))
#define MAX(a,b) ((a) > (b) ? (a) : (b))
typedef struct Item Item;
struct Item {
char *text;
Item *next; /* traverses all items */
Item *left, *right; /* traverses matching items */
int32_t width;
};
typedef enum {
LEFT,
RIGHT,
CENTRE
} TextPosition;
struct {
int32_t width;
int32_t height;
int32_t text_height;
int32_t text_y;
int32_t input_field;
int32_t scroll_left;
int32_t matches;
int32_t scroll_right;
} window_config;
const char *progname;
static uint32_t color_bg = 0x222222ff;
static uint32_t color_fg = 0xbbbbbbff;
static uint32_t color_input_bg = 0x222222ff;
static uint32_t color_input_fg = 0xbbbbbbff;
static uint32_t color_prompt_bg = 0x005577ff;
static uint32_t color_prompt_fg = 0xeeeeeeff;
static uint32_t color_selected_bg = 0x005577ff;
static uint32_t color_selected_fg = 0xeeeeeeff;
static int32_t panel_height = 20;
static void appenditem(Item *item, Item **list, Item **last);
static char *fstrstr(const char *s, const char *sub);
static void insert(const char *s, ssize_t n);
static void match(void);
static size_t nextrune(int incr);
static void readstdin(void);
static void alarmhandler(int signum);
/* static void handle_return(char* value); */
static void usage(void);
static int retcode = EXIT_SUCCESS;
static int selected_monitor = 0;
static char *selected_monitor_name = 0;
static char text[BUFSIZ];
static char text_[BUFSIZ];
static int itemcount = 0;
static int lines = 0;
static int timeout = 3;
static size_t cursor = 0;
static const char *prompt = NULL;
static bool message = false;
static bool nostdin = false;
static bool returnearly = false;
static bool show_in_bottom = false;
static TextPosition messageposition = LEFT;
static Item *items = NULL;
static Item *matches, *sel;
static Item *prev, *curr, *next;
static Item *leftmost, *rightmost;
static char *font = "Mono";
static int (*fstrncmp)(const char *, const char *, size_t) = strncmp;
void
insert(const char *s, ssize_t n) {
if(strlen(text) + n > sizeof text - 1)
return;
memmove(text + cursor + n, text + cursor, sizeof text - cursor - MAX(n, 0));
if(n > 0)
memcpy(text + cursor, s, n);
cursor += n;
match();
}
void keyrepeat(struct dmenu_panel *panel) {
if (panel->on_keyevent) {
panel->on_keyevent(panel, panel->repeat_key_state, panel->repeat_sym,
panel->keyboard.control, panel->keyboard.shift);
}
}
void keypress(struct dmenu_panel *panel, enum wl_keyboard_key_state state,
xkb_keysym_t sym, bool ctrl, bool shft) {
char buf[8];
size_t len = strlen(text);
if (state != WL_KEYBOARD_KEY_STATE_PRESSED) return;
if (ctrl) {
switch (xkb_keysym_to_lower(sym)) {
case XKB_KEY_a:
sym = XKB_KEY_Home;
break;
case XKB_KEY_e:
sym = XKB_KEY_End;
break;
case XKB_KEY_f:
case XKB_KEY_n:
sym = XKB_KEY_Right;
break;
case XKB_KEY_b:
case XKB_KEY_p:
sym = XKB_KEY_Left;
break;
case XKB_KEY_h:
sym = XKB_KEY_BackSpace;
break;
case XKB_KEY_j:
sym = XKB_KEY_Return;
break;
case XKB_KEY_g:
case XKB_KEY_c:
retcode = EXIT_FAILURE;
dmenu_close(panel);
return;
}
}
switch (sym) {
case XKB_KEY_KP_Enter: /* fallthrough */
case XKB_KEY_Return:
dmenu_close(panel);
fputs((sel && !shft) ? sel->text : text, stdout);
fflush(stdout);
break;
case XKB_KEY_Escape:
retcode = EXIT_FAILURE;
dmenu_close(panel);
break;
case XKB_KEY_Left:
if(cursor && (!sel || !sel->left)) {
cursor = nextrune(-1);
} if (sel && sel->left) {
sel = sel->left;
}
break;
case XKB_KEY_Right:
if (cursor < len) {
cursor = nextrune(+1);
} else if (cursor == len) {
if (sel && sel->right) sel = sel->right;
}
break;
case XKB_KEY_End:
if(cursor < len) {
cursor = len;
break;
}
while(sel && sel->right)
sel = sel->right;
break;
case XKB_KEY_Home:
if(sel == matches) {
cursor = 0;
break;
}
sel = curr = matches;
/* calcoffsets(); */
break;
case XKB_KEY_BackSpace:
if (cursor > 0)
insert(NULL, nextrune(-1) - cursor);
break;
case XKB_KEY_Delete:
if (cursor == len)
return;
cursor = nextrune(+1);
break;
case XKB_KEY_Tab:
if(!sel) return;
strncpy(text, sel->text, sizeof text);
cursor = strlen(text);
match();
break;
default:
if (xkb_keysym_to_utf8(sym, buf, 8)) {
insert(buf, strnlen(buf, 8));
}
}
dmenu_draw(panel);
}
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);
}
int32_t draw_text(cairo_t *cairo, int32_t width, int32_t height, const char *str,
int32_t x, int32_t scale, uint32_t
foreground_color, uint32_t background_color, int32_t padding) {
int32_t text_width, text_height;
get_text_size(cairo, font, &text_width, &text_height,
NULL, scale, false, str);
int32_t text_y = (height / 2.0) - (text_height / 2.0);
if (x + padding * scale + text_width + 30 * scale > width) {
/* if (x + padding * scale + text_width > width) { */
cairo_move_to(cairo, width, text_y);
pango_printf(cairo, font, scale, false, ">");
} else {
if (background_color) {
cairo_set_source_u32(cairo, background_color);
cairo_rectangle(cairo, x, 0, text_width + 2 * padding * scale, height);
cairo_fill(cairo);
}
cairo_move_to(cairo, x + padding * scale, text_y);
cairo_set_source_u32(cairo, foreground_color);
pango_printf(cairo, font, scale, false, str);
}
return x + text_width + 2 * padding * scale;
}
void draw(cairo_t *cairo, int32_t width, int32_t height, int32_t scale) {
int32_t x = window_config.input_field;
int32_t item_padding = 10;
int32_t text_width, text_height;
get_text_size(cairo, font, &text_width, &text_height, NULL, scale,
false, "Aj");
int32_t text_y = (height / 2.0) - (text_height / 2.0);
cairo_set_source_u32(cairo, color_bg);
cairo_paint(cairo);
if (prompt) {
x = draw_text(cairo, width, height, prompt, 0, scale, color_prompt_fg,
color_prompt_bg, 6);
window_config.input_field = x;
} else {
window_config.input_field = 0;
}
cairo_set_source_u32(cairo, color_input_bg);
cairo_rectangle(cairo, window_config.input_field, 0, 300 * scale, height);
cairo_fill(cairo);
draw_text(cairo, width, height, text, x, scale, color_input_fg, 0, 6);
{
/* draw cursor */
memset(text_, 0, BUFSIZ);
strncpy(text_, text, cursor);
int32_t text_width, text_height;
get_text_size(cairo, font, &text_width, &text_height, NULL, scale,
false, text_);
/* int32_t text_y = (height / 2.0) - (text_height / 2.0); */
int32_t padding = 6 * scale;
cairo_rectangle(cairo, x + padding + text_width, text_y,
scale, text_height);
cairo_fill(cairo);
}
x += 300 * scale;
/* Scroll indicator will be drawn later if required. */
int32_t scroll_indicator_pos = x;
x += 20 * scale;
if (matches) {
/* draw matches */
Item *item;
/* for (item = matches; item; item = item->right) { */
/* if (item->width == -1) { */
/* get_text_size(cairo, font, &item->width, NULL, NULL, scale, */
/* false, item->text); */
/* item->width += item_padding; */
/* /\* printf("%d ", item->width); *\/ */
/* } */
/* } */
/* /\* Figure out if we need to scroll. *\/ */
/* int32_t item_pos = x; */
/* bool found = false; */
/* rightmost = NULL; */
/* for (item = leftmost; item; item = item->right) { */
/* item_pos += item->width; */
/* if (item_pos >= (width - x - 80 * scale)) { */
/* rightmost = item->left; */
/* printf("rightmost: %s\n", item->left->text); */
/* found = true; */
/* break; */
/* } */
/* } */
for (item = matches; item; item = item->right) {
uint32_t bg_color = sel == item ? color_selected_bg : color_bg;
uint32_t fg_color = sel == item ? color_selected_fg : color_fg;
if (x < width) {
/* x = draw_text(cairo, width - 20 * scale, height, item->text, */
/* x, scale, fg_color, bg_color, item_padding); */
x = draw_text(cairo, width - 20 * scale, height, item->text,
x, scale, fg_color, bg_color, item_padding);
} else {
break;
}
}
if (leftmost != matches) {
cairo_move_to(cairo, scroll_indicator_pos, text_y);
pango_printf(cairo, font, scale, false, "<");
}
}
}
uint32_t parse_color(char *str) {
if (!str) eprintf("NULL as color value\n");
size_t len = strnlen(str, BUFSIZ);
if ((len != 7 && len != 9) || str[0] != '#')
eprintf("Color format must be '#rrggbb[aa]'\n");
uint32_t _val = strtol(&str[1], NULL, 16);
uint32_t color = 0x000000ff;
if (len == 9) /* Alpha specified */
color = _val;
else /* No alpha specified, assume full opacity */
color = (_val << 8) + 0xff;
return color;
}
int
main(int argc, char **argv) {
int i;
progname = "dmenu";
for (i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-v") || !strcmp(argv[1], "--version")) {
fputs("dmenu-wl-" VERSION
", © 2006-2018 dmenu engineers, see LICENSE for details\n",
stdout);
exit(EXIT_SUCCESS);
} else if (!strcmp(argv[i], "-b") || !strcmp(argv[i], "--bottom"))
show_in_bottom = true;
else if (!strcmp(argv[i], "-e") || !strcmp(argv[i], "--echo"))
message = true;
else if (!strcmp(argv[i], "-ec") || !strcmp(argv[i], "--echo-centre"))
message = true, messageposition = CENTRE;
else if (!strcmp(argv[i], "-er") || !strcmp(argv[i], "--echo-right"))
message = true, messageposition = RIGHT;
else if (!strcmp(argv[i], "-i") || !strcmp(argv[i], "--insensitive"))
fstrncmp = strncasecmp;
else if (!strcmp(argv[i], "-r") || !strcmp(argv[i], "--return-early"))
returnearly = true;
else if (i == argc - 1) {
printf("2\n");
usage();
}
/* opts that need 1 arg */
else if (!strcmp(argv[i], "-et") || !strcmp(argv[i], "--echo-timeout"))
timeout = atoi(argv[++i]);
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--height"))
panel_height = atoi(argv[++i]);
else if (!strcmp(argv[i], "-l") || !strcmp(argv[i], "--lines"))
lines = atoi(argv[++i]);
else if (!strcmp(argv[i], "-m") || !strcmp(argv[i], "--monitor")) {
++i;
bool is_num = true;
for (int j = 0; j < strlen(argv[i]); ++j) {
if (!isdigit(argv[i][j])) {
is_num = false;
break;
}
}
if (is_num) {
selected_monitor = atoi(argv[i]);
} else {
selected_monitor = -1;
selected_monitor_name = argv[i];
}
}
else if (!strcmp(argv[i], "-p") || !strcmp(argv[i], "--prompt"))
prompt = argv[++i];
else if (!strcmp(argv[i], "-po") || !strcmp(argv[i], "--prompt-only"))
prompt = argv[++i], nostdin = true;
else if (!strcmp(argv[i], "-fn") || !strcmp(argv[i], "--font-name"))
font = argv[++i];
else if (!strcmp(argv[i], "-nb") || !strcmp(argv[i], "--normal-background"))
color_bg = color_input_bg = parse_color(argv[++i]);
else if (!strcmp(argv[i], "-nf") || !strcmp(argv[i], "--normal-foreground"))
color_fg = color_input_fg = parse_color(argv[++i]);
else if (!strcmp(argv[i], "-sb") ||
!strcmp(argv[i], "--selected-background"))
color_prompt_bg = color_selected_bg = parse_color(argv[++i]);
else if (!strcmp(argv[i], "-sf") ||
!strcmp(argv[i], "--selected-foreground"))
color_prompt_fg = color_selected_fg = parse_color(argv[++i]);
else {
usage();
}
}
if (message) {
signal(SIGALRM, alarmhandler);
alarm(timeout);
}
if(!nostdin) {
readstdin();
}
struct dmenu_panel dmenu;
dmenu.selected_monitor = selected_monitor;
dmenu.selected_monitor_name = selected_monitor_name;
dmenu_init_panel(&dmenu, panel_height, show_in_bottom);
dmenu.on_keyevent = keypress;
dmenu.on_keyrepeat = keyrepeat;
dmenu.draw = draw;
match();
struct monitor_info *monitor = dmenu.monitor;
double factor = monitor->scale / ((double)monitor->physical_width / monitor->logical_width);
window_config.height =round_to_int(dmenu.height / ((double)monitor->physical_width
/ monitor->logical_width));
window_config.height *= monitor->scale;
window_config.width = round_to_int(monitor->physical_width * factor);
get_text_size(dmenu.surface.cairo, font, NULL, &window_config.text_height,
NULL, monitor->scale, false, "Aj");
window_config.text_y = (window_config.height / 2.0) - (window_config.text_height / 2.0);
dmenu_show(&dmenu);
return retcode;
}
void
appenditem(Item *item, Item **list, Item **last) {
if(!*last)
*list = item;
else
(*last)->right = item;
item->left = *last;
item->right = NULL;
*last = item;
}
char *
fstrstr(const char *s, const char *sub) {
size_t len;
for(len = strlen(sub); *s; s++)
if(!fstrncmp(s, sub, len))
return (char *)s;
return NULL;
}
void
match(void) {
size_t len;
Item *item, *itemend, *lexact, *lprefix, *lsubstr, *exactend, *prefixend, *substrend;
rightmost = leftmost = NULL;
len = strlen(text);
matches = lexact = lprefix = lsubstr = itemend = exactend = prefixend = substrend = NULL;
for(item = items; item; item = item->next)
if(!fstrncmp(text, item->text, len + 1)) {
appenditem(item, &lexact, &exactend);
}
else if(!fstrncmp(text, item->text, len)) {
appenditem(item, &lprefix, &prefixend);
}
else if(fstrstr(item->text, text)) {
appenditem(item, &lsubstr, &substrend);
}
if(lexact) {
matches = lexact;
itemend = exactend;
}
if(lprefix) {
if(itemend) {
itemend->right = lprefix;
lprefix->left = itemend;
}
else
matches = lprefix;
itemend = prefixend;
}
if(lsubstr) {
if(itemend) {
itemend->right = lsubstr;
lsubstr->left = itemend;
}
else
matches = lsubstr;
}
curr = prev = next = sel = matches;
/* calcoffsets(); */
leftmost = matches;
if(returnearly && !curr->right) {
/* handle_return(curr->text); */
}
}
size_t
nextrune(int incr) {
size_t n, len;
len = strlen(text);
for(n = cursor + incr; n >= 0 && n < len && (text[n] & 0xc0) == 0x80; n += incr);
return n;
}
void
readstdin(void) {
char buf[sizeof text], *p;
Item *item, **end;
for(end = &items; fgets(buf, sizeof buf, stdin); *end = item, end = &item->next) {
itemcount++;
if((p = strchr(buf, '\n'))) {
*p = '\0';
}
if(!(item = malloc(sizeof *item))) {
eprintf("cannot malloc %u bytes\n", sizeof *item);
}
item->width = -1;
if(!(item->text = strdup(buf))) {
eprintf("cannot strdup %u bytes\n", strlen(buf)+1);
}
item->next = item->left = item->right = NULL;
/* inputw = MAX(inputw, textw(dc, item->text)); */
}
}
void
alarmhandler(int signum) {
exit(EXIT_SUCCESS);
}
void
usage(void) {
printf("Usage: dmenu [OPTION]...\n");
printf("Display newline-separated input stdin as a menubar\n");
printf("\n");
printf(" -e, --echo display text from stdin with no user\n");
printf(" interaction\n");
printf(" -ec, --echo-centre same as -e but align text centrally\n");
printf(" -er, --echo-right same as -e but align text right\n");
printf(" -et, --echo-timeout SECS close the message after SEC seconds\n");
printf(" when using -e, -ec, or -er\n");
printf(" -b, --bottom dmenu appears at the bottom of the screen\n");
printf(" -h, --height N set dmenu to be N pixels high\n");
printf(" -i, --insensitive dmenu matches menu items case insensitively\n");
printf(" -l, --lines LINES dmenu lists items vertically, within the\n");
printf(" given number of lines\n");
printf(" -m, --monitor MONITOR dmenu appears on the given Xinerama screen\n");
printf(" (does nothing on wayland, supported for)\n");
printf(" compatibility with dmenu.\n");
printf(" -p, --prompt PROMPT prompt to be displayed to the left of the\n");
printf(" input field\n");
printf(" -po, --prompt-only PROMPT same as -p but don't wait for stdin\n");
printf(" useful for a prompt with no menu\n");
printf(" -r, --return-early return as soon as a single match is found\n");
printf(" -fn, --font-name FONT font or font set to be used\n");
printf(" -nb, --normal-background COLOR normal background color\n");
printf(" #RRGGBB and #RRGGBBAA supported\n");
printf(" -nf, --normal-foreground COLOR normal foreground color\n");
printf(" -sb, --selected-background COLOR selected background color\n");
printf(" -sf, --selected-foreground COLOR selected foreground color\n");
printf(" -v, --version display version information\n");
exit(EXIT_FAILURE);
}