From 9499f8bf03a5959071efd79d9f097c2868b4cc74 Mon Sep 17 00:00:00 2001 From: Out Of Ideas Date: Sun, 3 Mar 2024 12:49:42 -0600 Subject: [PATCH] Copy files over --- .dir-locals.el | 8 + LICENSE | 29 ++ README.md | 83 ++++ dmenu-wl.1 | 125 ++++++ dmenu-wl_run | 2 + dmenu.c | 612 +++++++++++++++++++++++++++++ dmenu_path.c | 102 +++++ draw.c | 659 ++++++++++++++++++++++++++++++++ draw.h | 106 +++++ meson.build | 82 ++++ wlr-layer-shell-unstable-v1.xml | 285 ++++++++++++++ 11 files changed, 2093 insertions(+) create mode 100644 .dir-locals.el create mode 100644 LICENSE create mode 100644 README.md create mode 100644 dmenu-wl.1 create mode 100755 dmenu-wl_run create mode 100644 dmenu.c create mode 100644 dmenu_path.c create mode 100644 draw.c create mode 100644 draw.h create mode 100644 meson.build create mode 100644 wlr-layer-shell-unstable-v1.xml diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..d6444ff --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,8 @@ + +((nil . ((tab-width . 4) + (c-basic-offset . 4) + (sentence-end-double-space . t) + (indent-tabs-mode . t) + (fill-column . 80))) + (c-mode . ((c-file-style . "GNU") + (c-noise-macro-names . ("UNINIT" "CALLBACK" "ALIGN_STACK"))))) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3b6ffa --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +MIT/X Consortium License + +© 2018-2019 Henrik Nyman +© 2012 Steve Engledow +© 2010 Connor Lane Smith +© 2006-2010 Anselm R Garbe +© 2009 Gottox +© 2009 Markus Schnalke +© 2009 Evan Gates +© 2006-2008 Sander van Dijk +© 2006-2007 Michał Janeczek + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7866de6 --- /dev/null +++ b/README.md @@ -0,0 +1,83 @@ +# dmenu-wl - dynamic menu +dmenu-wl is an efficient dynamic menu for wayland (wlroots). + +## TODO +Missing dmenu (for X) features: +- Echo (non-interactive use) +- Handling pagination +- Vertical layout +- Return-early +- Control-Enter handling +- Keyrepeat +- Choice (-c) -mode + +Other TODO items: +- Cleaner exiting +- Generate protocol files with `wayland-scanner` + +## Requirements +Requires a compositor which implements wlr-layer-shell and xdg-output +protocols. Basically this means a wlroots-based compositor is needed. +Gnome and KDE are therefore [not supported](https://github.com/nyyManni/dmenu-wayland/issues/16). +Tested with sway 1.0. + +Required libraries (and headers): +- wayland-client +- cairo +- pango-1.0 +- pangocairo-1.0 +- xkbcommon +- glib-2.0 +- gobject-2.0 + + +## Installation +``` + mkdir build + meson build + ninja -C build + sudo ninja -C build install +``` + +## Running dmenu-wl ... + +### ... as the application launcher in Sway + +Add to sway configuration (`~/.config/sway/config`) to run the launcher on Win+D. + + bindsym $mod+d exec dmenu-wl_run -i + +### ... from the command-line + +See the man page for details. +``` +Usage: dmenu-wl [OPTION]... + +Display newline-separated input stdin as a menubar + + -e, --echo display text from stdin with no user + interaction + -ec, --echo-centre same as -e but align text centrally + -er, --echo-right same as -e but align text right + -et, --echo-timeout SECS close the message after SEC seconds + when using -e, -ec, or -er + -b, --bottom dmenu appears at the bottom of the screen + -h, --height N set dmenu to be N pixels high + -i, --insensitive dmenu matches menu items case insensitively + -l, --lines LINES dmenu lists items vertically, within the + given number of lines + -m, --monitor MONITOR dmenu appears on the given monitor + (0-based index or monitor name) + -p, --prompt PROMPT prompt to be displayed to the left of the + input field + -po, --prompt-only PROMPT same as -p but don't wait for stdin + useful for a prompt with no menu + -r, --return-early return as soon as a single match is found + -fn, --font-name FONT font or font set to be used + -nb, --normal-background COLOR normal background color + #RRGGBB and #RRGGBBAA supported + -nf, --normal-foreground COLOR normal foreground color + -sb, --selected-background COLOR selected background color + -sf, --selected-foreground COLOR selected foreground color + -v, --version display version information +``` diff --git a/dmenu-wl.1 b/dmenu-wl.1 new file mode 100644 index 0000000..67e120a --- /dev/null +++ b/dmenu-wl.1 @@ -0,0 +1,125 @@ +.TH DMENU 1 dmenu-wl\-VERSION +.SH NAME +dmenu-wl \- dynamic menu +.SH SYNOPSIS +.B dmenu-wl +.RB [ \-b ] +.RB [ \-i ] +.RB [ \-l +.IR lines ] +.RB [ \-m +.IR monitor ] +.RB [ \-p +.IR prompt ] +.RB [ \-fn +.IR font ] +.RB [ \-nb +.IR color ] +.RB [ \-nf +.IR color ] +.RB [ \-sb +.IR color ] +.RB [ \-sf +.IR color ] +.RB [ \-v ] +.P +.BR dmenu-wl_run " ..." +.P +.B dmenu-wl_path +.SH DESCRIPTION +.B dmenu-wl +is a dynamic menu for Wayland, originally designed for X and +.BR dwm (1). +It manages huge numbers of user-defined menu items efficiently. +.P +dmenu-wl reads a list of newline-separated items from standard input and creates a +menu (or displays a notification if using -e). When the user selects an item or +enters any text and presses Return, their choice is printed to standard output +and dmenu-wl terminates. +.P +.B dmenu-wl_run +is a dmenu-wl script used by dwm which lists programs in the user's PATH and +executes the selected item. +.P +.B dmenu-wl_path +is a program used by dmenu-wl_run to find and cache a list of executables. +.SH OPTIONS +.B \-e +dmenu displays text from stdin with no user interaction. +.TP +.B \-ec +same as -e but text is aligned centrally. +.TP +.B \-er +same as -e but text is aligned to the right. +.TP +.BI \-et " secs " +when using -e, close the message after the given number of seconds +.TP +.B \-b +dmenu appears at the bottom of the screen. +.TP +.BI \-h " height" +dmenu is drawn at least the given number of pixels high. +.TP +.B \-i +dmenu matches menu items case insensitively. +.TP +.BI \-l " lines" +dmenu lists items vertically, within the given number of lines. +.TP +.BI \-m " monitor" +dmenu appears on the given monitor (0-based index or monitor name) +.TP +.BI \-p " prompt" +defines the prompt to be displayed to the left of the input field. +.TP +.BI \-po " prompt" +same as -p but don't wait for stdin - useful for a prompt with no selection options. +.TP +.B \-r +Return as soon as a single match is found. i.e. don't wait for the user to press return. +.TP +.BI \-fn " font" +defines the font or font set used. +.TP +.BI \-nb " color" +defines the normal background color. +.IR #RRGGBB and +.IR #RRGGBBAA +are supported. +.TP +.BI \-nf " color" +defines the normal foreground color. +.TP +.BI \-sb " color" +defines the selected background color. +.TP +.BI \-sf " color" +defines the selected foreground color. +.TP +.B \-v +prints version information to standard output, then exits. +.SH USAGE +dmenu-wl is completely controlled by the keyboard. Besides standard Unix line +editing and item selection (Up/Down/Left/Right, PageUp/PageDown, Home/End), the +following keys are recognized: +.TP +.B Tab (Control\-i) +Copy the selected item to the input field. +.TP +.B Return (Control\-j) +Confirm selection. Prints the selected item to standard output and exits, +returning success. +.TP +.B Shift\-Return (Control\-Shift\-j) +Confirm input. Prints the input text to standard output and exits, returning +success. +.TP +.B Escape (Control\-c) +Exit without selecting an item, returning failure. +.TP +.B Control\-y +Paste the current selection into the input field. +.SH SEE ALSO +.BR dwm (1) diff --git a/dmenu-wl_run b/dmenu-wl_run new file mode 100755 index 0000000..94b6f3a --- /dev/null +++ b/dmenu-wl_run @@ -0,0 +1,2 @@ +#!/bin/sh +dmenu-wl_path | dmenu-wl "$@" | ${SHELL:-"/bin/sh"} & diff --git a/dmenu.c b/dmenu.c new file mode 100644 index 0000000..0b5a7ed --- /dev/null +++ b/dmenu.c @@ -0,0 +1,612 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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); +} diff --git a/dmenu_path.c b/dmenu_path.c new file mode 100644 index 0000000..8df2667 --- /dev/null +++ b/dmenu_path.c @@ -0,0 +1,102 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#define CACHE ".dmenu_cache" + +static void die(const char *s); +static int qstrcmp(const void *a, const void *b); +static void scan(void); +static int uptodate(void); + +static char **items = NULL; +static const char *home, *path; + +int +main(void) { + if(!(home = getenv("HOME"))) + die("no $HOME"); + if(!(path = getenv("PATH"))) + die("no $PATH"); + if(chdir(home) < 0) + die("chdir failed"); + if(uptodate()) { + execlp("cat", "cat", CACHE, NULL); + die("exec failed"); + } + scan(); + return EXIT_SUCCESS; +} + +void +die(const char *s) { + fprintf(stderr, "dmenu_path: %s\n", s); + exit(EXIT_FAILURE); +} + +int +qstrcmp(const void *a, const void *b) { + return strcmp(*(const char **)a, *(const char **)b); +} + +void +scan(void) { + char buf[PATH_MAX]; + char *dir, *p; + size_t i, count; + struct dirent *ent; + DIR *dp; + FILE *cache; + + count = 0; + if(!(p = strdup(path))) + die("strdup failed"); + for(dir = strtok(p, ":"); dir; dir = strtok(NULL, ":")) { + if(!(dp = opendir(dir))) + continue; + while((ent = readdir(dp))) { + snprintf(buf, sizeof buf, "%s/%s", dir, ent->d_name); + if(ent->d_name[0] == '.' || access(buf, X_OK) < 0) + continue; + if(!(items = realloc(items, ++count * sizeof *items))) + die("malloc failed"); + if(!(items[count-1] = strdup(ent->d_name))) + die("strdup failed"); + } + closedir(dp); + } + qsort(items, count, sizeof *items, qstrcmp); + if(!(cache = fopen(CACHE, "w"))) + die("open failed"); + for(i = 0; i < count; i++) { + if(i > 0 && !strcmp(items[i], items[i-1])) + continue; + fprintf(cache, "%s\n", items[i]); + fprintf(stdout, "%s\n", items[i]); + } + fclose(cache); + free(p); +} + +int +uptodate(void) { + char *dir, *p; + time_t mtime; + struct stat st; + + if(stat(CACHE, &st) < 0) + return 0; + mtime = st.st_mtime; + if(!(p = strdup(path))) + die("strdup failed"); + for(dir = strtok(p, ":"); dir; dir = strtok(NULL, ":")) + if(!stat(dir, &st) && st.st_mtime > mtime) + return 0; + free(p); + return 1; +} diff --git a/draw.c b/draw.c new file mode 100644 index 0000000..f90570e --- /dev/null +++ b/draw.c @@ -0,0 +1,659 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "draw.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include "xdg-shell-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" + + +static const char overflow[] = "[buffer overflow]"; +static const int max_chars = 16384; + +struct monitor_info *monitors[16] = {0}; +static int n_monitors = 0; + +int32_t round_to_int(double val) { + return (int32_t)(val + 0.5); +} + +static void randname(char *buf) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + long r = ts.tv_nsec; + for (int i = 0; i < 6; ++i) { + buf[i] = 'A'+(r&15)+(r&16)*2; + r >>= 5; + } +} + +static int anonymous_shm_open(void) { + char name[] = "/dmenu-XXXXXX"; + int retries = 100; + + do { + randname(name + strlen(name) - 6); + + --retries; + // shm_open guarantees that O_CLOEXEC is set + int fd = shm_open(name, O_RDWR | O_CREAT | O_EXCL, 0600); + if (fd >= 0) { + shm_unlink(name); + return fd; + } + } while (retries > 0 && errno == EEXIST); + + return -1; +} + +int create_shm_file(off_t size) { + int fd = anonymous_shm_open(); + if (fd < 0) { + return fd; + } + + if (ftruncate(fd, size) < 0) { + close(fd); + return -1; + } + + return fd; +} + +PangoLayout *get_pango_layout(cairo_t *cairo, const char *font, + const char *text, double scale, bool markup) { + PangoLayout *layout = pango_cairo_create_layout(cairo); + PangoAttrList *attrs; + if (markup) { + char *buf; + GError *error = NULL; + if (pango_parse_markup(text, -1, 0, &attrs, &buf, NULL, &error)) { + pango_layout_set_text(layout, buf, -1); + free(buf); + } else { + /* wlr_log(WLR_ERROR, "pango_parse_markup '%s' -> error %s", text, */ + /* error->message); */ + g_error_free(error); + markup = false; // fallback to plain text + } + } + if (!markup) { + attrs = pango_attr_list_new(); + pango_layout_set_text(layout, text, -1); + } + + pango_attr_list_insert(attrs, pango_attr_scale_new(scale)); + PangoFontDescription *desc = pango_font_description_from_string(font); + pango_layout_set_font_description(layout, desc); + pango_layout_set_single_paragraph_mode(layout, 1); + pango_layout_set_attributes(layout, attrs); + pango_attr_list_unref(attrs); + pango_font_description_free(desc); + return layout; +} + +void get_text_size(cairo_t *cairo, const char *font, int *width, int *height, + int *baseline, double scale, bool markup, const char *fmt, ...) { + char buf[max_chars]; + + va_list args; + va_start(args, fmt); + if (vsnprintf(buf, sizeof(buf), fmt, args) >= max_chars) { + strcpy(&buf[sizeof(buf) - sizeof(overflow)], overflow); + } + va_end(args); + + PangoLayout *layout = get_pango_layout(cairo, font, buf, scale, markup); + pango_cairo_update_layout(cairo, layout); + pango_layout_get_pixel_size(layout, width, height); + if (baseline) { + *baseline = pango_layout_get_baseline(layout) / PANGO_SCALE; + } + g_object_unref(layout); +} + +void pango_printf(cairo_t *cairo, const char *font, + double scale, bool markup, const char *fmt, ...) { + char buf[max_chars]; + + va_list args; + va_start(args, fmt); + if (vsnprintf(buf, sizeof(buf), fmt, args) >= max_chars) { + strcpy(&buf[sizeof(buf) - sizeof(overflow)], overflow); + } + va_end(args); + + PangoLayout *layout = get_pango_layout(cairo, font, buf, scale, markup); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_get_font_options(cairo, fo); + pango_cairo_context_set_font_options(pango_layout_get_context(layout), fo); + cairo_font_options_destroy(fo); + pango_cairo_update_layout(cairo, layout); + pango_cairo_show_layout(cairo, layout); + g_object_unref(layout); +} + + +void dmenu_draw(struct dmenu_panel *panel) { + + cairo_t *cairo = panel->surface.cairo; + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + struct monitor_info *m = panel->monitor; + double factor = m->scale / ((double)m->physical_width + / m->logical_width); + + int32_t width = round_to_int(m->physical_width * factor); + int32_t height = panel->height * m->scale; + + if (panel->draw) { + panel->draw(cairo, width, height, m->scale); + } + wl_surface_attach(panel->surface.surface, panel->surface.buffer, 0, 0); + zwlr_layer_surface_v1_set_keyboard_interactivity(panel->surface.layer_surface, true); + wl_surface_damage(panel->surface.surface, 0, 0, m->logical_width, panel->height); + wl_surface_commit(panel->surface.surface); + +} + +cairo_subpixel_order_t to_cairo_subpixel_order(enum wl_output_subpixel subpixel) { + switch (subpixel) { + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_RGB: + return CAIRO_SUBPIXEL_ORDER_RGB; + case WL_OUTPUT_SUBPIXEL_HORIZONTAL_BGR: + return CAIRO_SUBPIXEL_ORDER_BGR; + case WL_OUTPUT_SUBPIXEL_VERTICAL_RGB: + return CAIRO_SUBPIXEL_ORDER_VRGB; + case WL_OUTPUT_SUBPIXEL_VERTICAL_BGR: + return CAIRO_SUBPIXEL_ORDER_VBGR; + default: + return CAIRO_SUBPIXEL_ORDER_DEFAULT; + } + return CAIRO_SUBPIXEL_ORDER_DEFAULT; +} + +void +eprintf(const char *fmt, ...) { + va_list ap; + + fprintf(stderr, "%s: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +static void layer_surface_configure(void *data, + struct zwlr_layer_surface_v1 *surface, + uint32_t serial, uint32_t width, uint32_t height) { + zwlr_layer_surface_v1_ack_configure(surface, serial); +} + +static void layer_surface_closed(void *_data, + struct zwlr_layer_surface_v1 *surface) { +} + +struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + + +int32_t subpixel; +int32_t physical_height; + + +static void output_geometry(void *data, struct wl_output *wl_output, int32_t x, + int32_t y, int32_t width_mm, int32_t height_mm, int32_t subpixel, + const char *make, const char *model, int32_t transform) { + struct monitor_info *monitor = data; + monitor->subpixel = subpixel; +} + +static void output_mode(void *data, struct wl_output *wl_output, uint32_t flags, + int32_t width, int32_t height, int32_t refresh) { + struct monitor_info *monitor = data; + monitor->physical_width = width; + monitor->physical_height = height; +} + +static void output_done(void *data, struct wl_output *wl_output) { +} + +static void output_scale(void *data, struct wl_output *wl_output, + int32_t factor) { + struct monitor_info *monitor = data; + monitor->scale = factor; +} + +struct wl_output_listener output_listener = { + .geometry = output_geometry, + .mode = output_mode, + .done = output_done, + .scale = output_scale, +}; +static void xdg_output_handle_logical_position(void *data, + struct zxdg_output_v1 *xdg_output, int32_t x, int32_t y) { + // Who cares +} + +static void xdg_output_handle_logical_size(void *data, + struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) { + struct monitor_info *monitor = data; + + monitor->logical_width = width; + monitor->logical_height = height; +} + +static void xdg_output_handle_done(void *data, + struct zxdg_output_v1 *xdg_output) { + // Who cares +} + +static void xdg_output_handle_name(void *data, + struct zxdg_output_v1 *xdg_output, const char *name) { + struct monitor_info *monitor = data; + strncpy(monitor->name, name, MAX_MONITOR_NAME_LEN); +} + +static void xdg_output_handle_description(void *data, + struct zxdg_output_v1 *xdg_output, const char *description) { +} + +struct zxdg_output_v1_listener xdg_output_listener = { + .logical_position = xdg_output_handle_logical_position, + .logical_size = xdg_output_handle_logical_size, + .done = xdg_output_handle_done, + .name = xdg_output_handle_name, + .description = xdg_output_handle_description, +}; + +static void keyboard_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) { + + struct dmenu_panel *panel = data; + + panel->keyboard.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) { + close(fd); + exit(1); + } + char *map_shm = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + if (map_shm == MAP_FAILED) { + close(fd); + exit(1); + } + panel->keyboard.xkb_keymap = xkb_keymap_new_from_string( + panel->keyboard.xkb_context, map_shm, XKB_KEYMAP_FORMAT_TEXT_V1, 0); + munmap(map_shm, size); + close(fd); + + panel->keyboard.xkb_state = xkb_state_new(panel->keyboard.xkb_keymap); +} + +static void keyboard_enter(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface, struct wl_array *keys) { + // Who cares +} + +static void keyboard_leave(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, struct wl_surface *surface) { + // Who cares +} + +static void keyboard_repeat(struct dmenu_panel *panel) { + if (panel->on_keyrepeat) { + panel->on_keyrepeat(panel); + } + + struct itimerspec spec = { 0 }; + spec.it_value.tv_sec = panel->repeat_period / 1000; + spec.it_value.tv_nsec = (panel->repeat_period % 1000) * 1000000l; + timerfd_settime(panel->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 dmenu_panel *panel = data; + + enum wl_keyboard_key_state key_state = _key_state; + xkb_keysym_t sym = xkb_state_key_get_one_sym(panel->keyboard.xkb_state, key + 8); + if (panel->on_keyevent) { + panel->on_keyevent(panel, key_state, sym, panel->keyboard.control, + panel->keyboard.shift); + + if (key_state == WL_KEYBOARD_KEY_STATE_PRESSED && panel->repeat_period >= 0) { + panel->repeat_key_state = key_state; + panel->repeat_sym = sym; + + struct itimerspec spec = { 0 }; + spec.it_value.tv_sec = panel->repeat_delay / 1000; + spec.it_value.tv_nsec = (panel->repeat_delay % 1000) * 1000000l; + timerfd_settime(panel->repeat_timer, 0, &spec, NULL); + } else if (key_state == WL_KEYBOARD_KEY_STATE_RELEASED) { + struct itimerspec spec = { 0 }; + timerfd_settime(panel->repeat_timer, 0, &spec, NULL); + } + } +} + +static void keyboard_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) { + struct dmenu_panel *panel = data; + panel->repeat_delay = delay; + if (rate > 0) { + panel->repeat_period = 1000 / rate; + } else { + panel->repeat_period = -1; + } +} + +static void keyboard_modifiers (void *data, struct wl_keyboard *keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, + uint32_t group) { + struct dmenu_panel *panel = data; + xkb_state_update_mask(panel->keyboard.xkb_state, + mods_depressed, mods_latched, mods_locked, 0, 0, group); + panel->keyboard.control = xkb_state_mod_name_is_active(panel->keyboard.xkb_state, + XKB_MOD_NAME_CTRL, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + panel->keyboard.shift = xkb_state_mod_name_is_active(panel->keyboard.xkb_state, + XKB_MOD_NAME_SHIFT, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); +} +static const struct wl_keyboard_listener keyboard_listener = { + .keymap = keyboard_keymap, + .enter = keyboard_enter, + .leave = keyboard_leave, + .key = keyboard_key, + .modifiers = keyboard_modifiers, + .repeat_info = keyboard_repeat_info, +}; + +static void seat_handle_capabilities(void *data, struct wl_seat *wl_seat, + enum wl_seat_capability caps) { + struct dmenu_panel *panel = data; + if (caps & WL_SEAT_CAPABILITY_KEYBOARD) { + panel->keyboard.kbd = wl_seat_get_keyboard (panel->display_info.seat); + wl_keyboard_add_listener (panel->keyboard.kbd, &keyboard_listener, panel); + } +} +static void seat_handle_name(void *data, struct wl_seat *wl_seat, + const char *name) { + // Who cares +} + +const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = seat_handle_name, +}; + +void set_monitor_xdg_output(struct dmenu_panel *panel, struct monitor_info *monitor){ + monitor->xdg_output = + zxdg_output_manager_v1_get_xdg_output(panel->display_info.xdg_output_manager, + monitor->output); + zxdg_output_v1_add_listener(monitor->xdg_output, &xdg_output_listener, + monitor); +} + +static void handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) { + struct dmenu_panel *panel = data; + + if (strcmp(interface, wl_compositor_interface.name) == 0) { + panel->display_info.compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 4); + } else if (strcmp(interface, wl_seat_interface.name) == 0) { + panel->display_info.seat = wl_registry_bind (registry, name, &wl_seat_interface, 4); + wl_seat_add_listener (panel->display_info.seat, &seat_listener, panel); + } else if (strcmp(interface, wl_shm_interface.name) == 0) { + panel->surface.shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + } else if (strcmp(interface, wl_output_interface.name) == 0) { + + if(n_monitors >= 16) return; + + monitors[n_monitors] = malloc(sizeof(struct monitor_info)); + monitors[n_monitors]->panel = panel; + memset(monitors[n_monitors]->name, 0, MAX_MONITOR_NAME_LEN); + monitors[n_monitors]->output = wl_registry_bind(registry, name, &wl_output_interface, 2); + + wl_output_add_listener(monitors[n_monitors]->output, &output_listener, + monitors[n_monitors]); + + if (panel->display_info.xdg_output_manager != NULL) { + set_monitor_xdg_output(panel, monitors[n_monitors]); + } + n_monitors++; + + } else if (strcmp(interface, zwlr_layer_shell_v1_interface.name) == 0) { + panel->surface.layer_shell = wl_registry_bind(registry, name, &zwlr_layer_shell_v1_interface, 1); + + } else if (strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) { + panel->display_info.xdg_output_manager = wl_registry_bind(registry, name, + &zxdg_output_manager_v1_interface, 2); + + for(int m = 0; m < n_monitors; m++){ + set_monitor_xdg_output(panel, monitors[m]); + } + } + +} + +static void handle_global_remove(void *data, struct wl_registry *registry, + uint32_t name) { +} + +static void buffer_release(void *data, struct wl_buffer *wl_buffer) { +} + +static const struct wl_buffer_listener buffer_listener = { + .release = buffer_release +}; + +struct wl_buffer *dmenu_create_buffer(struct dmenu_panel *panel) { + struct monitor_info *m = panel->monitor; + double factor = m->scale / ((double)m->physical_width + / m->logical_width); + + int32_t width = round_to_int(m->physical_width * factor); + int32_t height = round_to_int(panel->height * m->scale); + + int stride = width * 4; + int size = stride * height; + + int fd = create_shm_file(size); + if (fd < 0) { + fprintf(stderr, "creating a buffer file for %d B failed: %m\n", size); + return NULL; + } + + panel->surface.shm_data = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (panel->surface.shm_data == MAP_FAILED) { + fprintf(stderr, "mmap failed: %m\n"); + close(fd); + return NULL; + } + + struct wl_shm_pool *pool = wl_shm_create_pool(panel->surface.shm, fd, size); + struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height, + stride, WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(pool); + + + wl_buffer_add_listener(buffer, &buffer_listener, panel); + + cairo_surface_t *s = cairo_image_surface_create_for_data(panel->surface.shm_data, + CAIRO_FORMAT_ARGB32, + width, height, width * 4); + panel->surface.cairo = cairo_create(s); + cairo_set_antialias(panel->surface.cairo, CAIRO_ANTIALIAS_BEST); + cairo_font_options_t *fo = cairo_font_options_create(); + cairo_font_options_set_hint_style(fo, CAIRO_HINT_STYLE_FULL); + cairo_font_options_set_antialias(fo, CAIRO_ANTIALIAS_SUBPIXEL); + cairo_font_options_set_subpixel_order(fo, to_cairo_subpixel_order(m->subpixel)); + cairo_set_font_options(panel->surface.cairo, fo); + cairo_font_options_destroy(fo); + cairo_save(panel->surface.cairo); + + return buffer; +} + +static const struct wl_registry_listener registry_listener = { + .global = handle_global, + .global_remove = handle_global_remove, +}; + + +void dmenu_init_panel(struct dmenu_panel *panel, int32_t height, bool bottom) { + if(!setlocale(LC_CTYPE, "")) + weprintf("no locale support\n"); + + if(!(panel->display_info.display = wl_display_connect(NULL))) + eprintf("cannot open display\n"); + + if ((panel->repeat_timer = timerfd_create(CLOCK_MONOTONIC, 0)) < 0) + eprintf("cannot create timer fd\n"); + + panel->height = height; + panel->keyboard.control = false; + panel->on_keyevent = NULL; + + struct wl_registry *registry = wl_display_get_registry(panel->display_info.display); + wl_registry_add_listener(registry, ®istry_listener, panel); + + wl_display_roundtrip(panel->display_info.display); + + /* Second roundtrip for xdg-output. Will populate display dimensions. */ + wl_display_roundtrip(panel->display_info.display); + + + panel->surface.surface = wl_compositor_create_surface(panel->display_info.compositor); + + panel->monitor = NULL; + if (!panel->selected_monitor_name) { + panel->monitor = monitors[panel->selected_monitor]; + } else { + for (int i = 0; i < n_monitors; ++i) { + if (monitors[i] && !strncmp(panel->selected_monitor_name, + monitors[i]->name, + MAX_MONITOR_NAME_LEN)) { + panel->monitor = monitors[i]; + break; + } + } + } + if (!panel->monitor) { + if (!panel->selected_monitor_name) + eprintf("No monitor with index %i available.\n", panel->selected_monitor); + else + eprintf("No monitor with name %s available.\n", panel->selected_monitor_name); + } + + panel->surface.buffer = dmenu_create_buffer(panel); + + if (!panel->surface.layer_shell) + eprintf("Compositor does not implement wlr-layer-shell protocol.\n"); + panel->surface.layer_surface = + zwlr_layer_shell_v1_get_layer_surface(panel->surface.layer_shell, + panel->surface.surface, + panel->monitor->output, + ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "panel"); + + zwlr_layer_surface_v1_set_size(panel->surface.layer_surface, + panel->monitor->logical_width, panel->height); + zwlr_layer_surface_v1_set_anchor(panel->surface.layer_surface, + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT | + (bottom ? ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM + : ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP)); + + + zwlr_layer_surface_v1_add_listener(panel->surface.layer_surface, + &layer_surface_listener, panel); + + wl_surface_set_buffer_scale(panel->surface.surface, + panel->monitor->scale); + wl_surface_commit(panel->surface.surface); + wl_display_roundtrip(panel->display_info.display); + + zwlr_layer_surface_v1_set_keyboard_interactivity(panel->surface.layer_surface, true); + + wl_surface_attach(panel->surface.surface, panel->surface.buffer, 0, 0); + wl_surface_commit(panel->surface.surface); +} + +void dmenu_show(struct dmenu_panel *dmenu) { + dmenu_draw(dmenu); + + zwlr_layer_surface_v1_set_keyboard_interactivity(dmenu->surface.layer_surface, true); + wl_surface_commit(dmenu->surface.surface); + + struct pollfd fds[] = { + { wl_display_get_fd(dmenu->display_info.display), POLLIN }, + { dmenu->repeat_timer, POLLIN }, + }; + const int nfds = sizeof(fds) / sizeof(*fds); + + wl_display_flush(dmenu->display_info.display); + + dmenu->running = true; + while (dmenu->running) { + if (wl_display_flush(dmenu->display_info.display) < 0) { + if (errno == EAGAIN) + continue; + break; + } + + if (poll(fds, nfds, -1) < 0) { + if (errno == EAGAIN) + continue; + break; + } + + if (fds[0].revents & POLLIN) { + if (wl_display_dispatch(dmenu->display_info.display) < 0) { + dmenu->running = false; + } + } + + if (fds[1].revents & POLLIN) { + keyboard_repeat(dmenu); + } + } + + /* dmenu_close called */ + wl_display_disconnect(dmenu->display_info.display); + +} +void dmenu_close(struct dmenu_panel *dmenu) { + dmenu->running = false; +} + + +void +weprintf(const char *fmt, ...) { + va_list ap; + + fprintf(stderr, "%s: warning: ", progname); + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); +} diff --git a/draw.h b/draw.h new file mode 100644 index 0000000..4cafaea --- /dev/null +++ b/draw.h @@ -0,0 +1,106 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include + +#define MAX_MONITOR_NAME_LEN 255 + +#define FG(dc, col) ((col)[(dc)->invert ? ColBG : ColFG]) +#define BG(dc, col) ((col)[(dc)->invert ? ColFG : ColBG]) + +enum { ColBG, ColFG, ColBorder, ColLast }; + +struct dmenu_panel; + +struct monitor_info { + int32_t physical_width; + int32_t physical_height; + int32_t logical_width; + int32_t logical_height; + double scale; + + char name[MAX_MONITOR_NAME_LEN]; + + enum wl_output_subpixel subpixel; + + struct zxdg_output_v1 *xdg_output; + + struct wl_output *output; + struct dmenu_panel *panel; +}; + +extern struct monitor_info *monitors[]; + +struct display_info { + struct zxdg_output_manager_v1 *xdg_output_manager; + struct wl_display * display; + struct wl_compositor *compositor; + struct wl_seat *seat; + +}; + +struct keyboard_info { + struct wl_keyboard *kbd; + struct xkb_context *xkb_context; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + bool control; + bool shift; +}; + +struct surface { + cairo_t *cairo; + struct wl_buffer *buffer; + struct wl_surface *surface; + struct wl_shm *shm; + void *shm_data; + struct zwlr_layer_shell_v1 *layer_shell; + struct zwlr_layer_surface_v1 *layer_surface; +}; + +struct dmenu_panel { + struct keyboard_info keyboard; + /* struct monitor_info monitor; */ + int selected_monitor; + char *selected_monitor_name; + + struct monitor_info *monitor; + struct display_info display_info; + + struct surface surface; + + void (*on_keyevent)(struct dmenu_panel *,enum wl_keyboard_key_state, + xkb_keysym_t, bool, bool); + void (*on_keyrepeat)(struct dmenu_panel *); + + void (*draw)(cairo_t *, int32_t, int32_t, int32_t); + + int32_t width; + int32_t height; + + int repeat_timer; + int repeat_delay; + int repeat_period; + enum wl_keyboard_key_state repeat_key_state; + xkb_keysym_t repeat_sym; + + bool running; +}; + +void dmenu_init_panel(struct dmenu_panel *, int32_t, bool); +void dmenu_draw(struct dmenu_panel *); +void dmenu_show(struct dmenu_panel *); +void dmenu_close(struct dmenu_panel *); + + +void pango_printf(cairo_t *cairo, const char *font, + double scale, bool markup, const char *fmt, ...); +void get_text_size(cairo_t *cairo, const char *font, int *width, int *height, + int *baseline, double scale, bool markup, const char *fmt, ...); +void eprintf(const char *fmt, ...); +void weprintf(const char *fmt, ...); +int32_t round_to_int(double val); + +extern const char *progname; diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..3ae6f69 --- /dev/null +++ b/meson.build @@ -0,0 +1,82 @@ +project( + 'dmenu-wl', + 'c', + version: '4.2.1', + license: 'MIT', + default_options: ['c_std=c99'] +) + +add_project_arguments( + [ + '-pedantic', + '-Wall', + '-D_DEFAULT_SOURCE', + '-DVERSION="@0@"'.format(meson.project_version()), + ], + language : 'c' +) + +cc = meson.get_compiler('c') + +cairo = dependency('cairo') +glib = dependency('glib-2.0') +gobject = dependency('gobject-2.0') +pango = dependency('pango') +pangocairo = dependency('pangocairo') +rt = cc.find_library('rt') +wayland_client = dependency('wayland-client') +wayland_protos = dependency('wayland-protocols') +wayland_scanner_dep = dependency('wayland-scanner') +wayland_scanner = find_program( + wayland_scanner_dep.get_pkgconfig_variable('wayland_scanner') +) +xkbcommon = dependency('xkbcommon') + +wl_protocol_dir = wayland_protos.get_pkgconfig_variable('pkgdatadir') + +protocols = [ + [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], + [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], + 'wlr-layer-shell-unstable-v1.xml' +] + +protos_src = [] +protos_headers = [] + +foreach p : protocols + xml = join_paths(p) + protos_src += custom_target( + xml.underscorify() + '_client_c', + input: xml, + output: '@BASENAME@-protocol.c', + command: [wayland_scanner, 'public-code', '@INPUT@', '@OUTPUT@'], + ) + protos_headers += custom_target( + xml.underscorify() + '_client_h', + input: xml, + output: '@BASENAME@-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) +endforeach + +dmenu_deps = [ + cairo, + glib, + gobject, + pango, + pangocairo, + rt, + wayland_client, + xkbcommon +] + +dmenu_src = ['dmenu.c', 'draw.c'] + protos_src + protos_headers + +executable('dmenu-wl', dmenu_src, dependencies: dmenu_deps, install: true) + +executable('dmenu-wl_path', 'dmenu_path.c', install: true) + +install_data('dmenu-wl_run', install_dir: get_option('bindir')) + +install_man('dmenu-wl.1') + diff --git a/wlr-layer-shell-unstable-v1.xml b/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 0000000..216e0d9 --- /dev/null +++ b/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,285 @@ + + + + Copyright © 2017 Drew DeVault + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (size, anchor, exclusive zone, margin, interactivity) + is double-buffered, and will be applied at the time wl_surface.commit of + the corresponding wl_surface is called. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthoginal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area of the surface + with other surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to an + edge, rather than a corner. The zone is the number of surface-local + coordinates from the edge that are considered exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive excluzive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Set to 1 to request that the seat send keyboard events to this layer + surface. For layers below the shell surface layer, the seat will use + normal focus semantics. For layers above the shell surface layers, the + seat will always give exclusive keyboard focus to the top-most layer + which has keyboard interactivity set to true. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Events is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + +