commit 6cef260109af45bae5ffe053eec81f064620d6e2 Author: sewn Date: Wed Jun 26 07:58:41 2024 +0300 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55fd519 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +mew +config.h +*.o +*-protocol.* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..921ddb0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,34 @@ +MIT/X Consortium License + +Copyright (c) 2023-2024 sewn +Copyright (c) 2015 Eric Pruitt +Copyright (c) 2024 Forrest Bushstone +Copyright (c) 2018-2019 Henrik Nyman +Copyright (c) 2006-2019 Anselm R Garbe +Copyright (c) 2006-2008 Sander van Dijk +Copyright (c) 2006-2007 Michał Janeczek +Copyright (c) 2007 Kris Maglione +Copyright (c) 2009 Gottox +Copyright (c) 2009 Markus Schnalke +Copyright (c) 2009 Evan Gates +Copyright (c) 2010-2012 Connor Lane Smith +Copyright (c) 2014-2022 Hiltjo Posthuma +Copyright (c) 2015-2019 Quentin Rameau + +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/Makefile b/Makefile new file mode 100644 index 0000000..f6c1add --- /dev/null +++ b/Makefile @@ -0,0 +1,69 @@ +# mew - dynamic menu +# See LICENSE file for copyright and license details. +.POSIX: + +# mew version +VERSION = 1.0 + +# pkg-config +PKG_CONFIG = pkg-config + +# paths +PREFIX = /usr/local +MANPREFIX = $(PREFIX)/share/man + +# includes and libs +PKGS = fcft pixman-1 wayland-client xkbcommon +INCS = `$(PKG_CONFIG) --cflags $(PKGS)` +LIBS = `$(PKG_CONFIG) --libs $(PKGS)` + +# flags +EMCPPFLAGS = -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" +EMCFLAGS = -pedantic -Wall $(INCS) $(EMCPPFLAGS) $(CFLAGS) +LDLIBS = $(LIBS) + +all: mew + +.c.o: + $(CC) -c $(EMCFLAGS) $< + +config.h: + cp config.def.h $@ + +mew.o: config.h wlr-layer-shell-unstable-v1-protocol.h xdg-shell-protocol.h + +mew: wlr-layer-shell-unstable-v1-protocol.o xdg-shell-protocol.o mew.o + $(CC) $(LDFLAGS) -o $@ wlr-layer-shell-unstable-v1-protocol.o xdg-shell-protocol.o mew.o $(LDLIBS) + +WAYLAND_PROTOCOLS = `$(PKG_CONFIG) --variable=pkgdatadir wayland-protocols` +WAYLAND_SCANNER = `$(PKG_CONFIG) --variable=wayland_scanner wayland-scanner` + +xdg-shell-protocol.h: + $(WAYLAND_SCANNER) client-header $(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@ +xdg-shell-protocol.c: + $(WAYLAND_SCANNER) private-code $(WAYLAND_PROTOCOLS)/stable/xdg-shell/xdg-shell.xml $@ + +wlr-layer-shell-unstable-v1-protocol.h: + $(WAYLAND_SCANNER) client-header wlr-layer-shell-unstable-v1.xml $@ +wlr-layer-shell-unstable-v1-protocol.c: + $(WAYLAND_SCANNER) private-code wlr-layer-shell-unstable-v1.xml $@ +wlr-layer-shell-unstable-v1-protocol.o: xdg-shell-protocol.o + +clean: + rm -f mew *.o *-protocol.* + +install: all + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f mew mew-run $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/mew + chmod 755 $(DESTDIR)$(PREFIX)/bin/mew-run + mkdir -p $(DESTDIR)$(MANPREFIX)/man1 + sed "s/VERSION/$(VERSION)/g" < mew.1 > $(DESTDIR)$(MANPREFIX)/man1/mew.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/mew.1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/mew\ + $(DESTDIR)$(PREFIX)/bin/mew-run\ + $(DESTDIR)$(MANPREFIX)/man1/mew.1 + +.PHONY: all clean install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..b2a99b3 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# mew +mew is a simple menu for Wayland. + +## Building +In order to build mew, ensure that you have the following dependencies: + +* fcft +* pkg-config +* wayland +* wayland-protocols +* xkbcommon + +Afterwards enter the following command to build and install mew +(if necessary as root): +``` +make +make install +``` + +# Usage +See the man page for details. \ No newline at end of file diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..105378e --- /dev/null +++ b/config.def.h @@ -0,0 +1,21 @@ +/* See LICENSE file for copyright and license details. */ +/* Default settings; can be overriden by command line. */ + +static int top = 1; /* -b option; if 0, appear at bottom */ +static const char *fonts[] = { "monospace:size=10" }; /* -f option overrides fonts[0] */ +static const char *prompt = NULL; /* -p option; prompt to the left of input field */ +static uint32_t colors[][2] = { + /* fg bg */ + [SchemeNorm] = { 0xbbbbbbff, 0x222222ff }, + [SchemeSel] = { 0xeeeeeeff, 0x005577ff }, + [SchemeOut] = { 0x000000ff, 0x00ffffff }, +}; + +/* -l option; if nonzero, use vertical list with given number of lines */ +static unsigned int lines = 0; + +/* + * Characters not considered part of a word while deleting words + * for example: " /?\"&[]" + */ +static const char worddelimiters[] = " "; diff --git a/drwl.h b/drwl.h new file mode 100644 index 0000000..ba894b3 --- /dev/null +++ b/drwl.h @@ -0,0 +1,306 @@ +/* + * drwl - https://codeberg.org/sewn/drwl + * See LICENSE file for copyright and license details. + */ +#pragma once + +#include +#include +#include + +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) + +enum { ColFg, ColBg }; /* colorscheme index */ + +typedef struct { + pixman_image_t *pix; + struct fcft_font *font; + uint32_t *scheme; +} Drwl; + +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 + +static const unsigned char utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const unsigned char utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const uint32_t utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const uint32_t utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +static inline uint32_t +utf8decodebyte(const char c, size_t *i) +{ + for (*i = 0; *i < (UTF_SIZ + 1); ++(*i)) + if (((unsigned char)c & utfmask[*i]) == utfbyte[*i]) + return (unsigned char)c & ~utfmask[*i]; + return 0; +} + +static inline size_t +utf8decode(const char *c, uint32_t *u) +{ + size_t i, j, len, type; + uint32_t udecoded; + + *u = UTF_INVALID; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < UTF_SIZ && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type) + return j; + } + if (j < len) + return 0; + *u = udecoded; + if (!BETWEEN(*u, utfmin[len], utfmax[len]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + return len; +} + +static int +drwl_init(void) +{ + fcft_set_scaling_filter(FCFT_SCALING_FILTER_LANCZOS3); + return fcft_init(FCFT_LOG_COLORIZE_AUTO, 0, FCFT_LOG_CLASS_ERROR); +} + +/* + * Caller must call drwl_init before drwl_create, and + * drwl_destroy on returned context after finalizing usage. + */ +static Drwl * +drwl_create(void) +{ + Drwl *drwl; + + if (!(drwl = calloc(1, sizeof(Drwl)))) + return NULL; + + return drwl; +} + +static void +drwl_setfont(Drwl *drwl, struct fcft_font *font) +{ + if (drwl) + drwl->font = font; +} + +/* + * Returned font is set within the drawing context if given. + * Caller must call drwl_destroy_font on returned font when done using it, + * otherwise use drwl_destroy when instead given a drwl context. + */ +static struct fcft_font * +drwl_load_font(Drwl *drwl, size_t fontcount, + const char *fonts[static fontcount], const char *attributes) +{ + struct fcft_font *font = fcft_from_name(fontcount, fonts, attributes); + if (drwl) + drwl_setfont(drwl, font); + return font; +} + +static void +drwl_destroy_font(struct fcft_font *font) +{ + fcft_destroy(font); +} + +static inline pixman_color_t +convert_color(uint32_t clr) +{ + return (pixman_color_t){ + ((clr >> 24) & 0xFF) * 0x101, + ((clr >> 16) & 0xFF) * 0x101, + ((clr >> 8) & 0xFF) * 0x101, + (clr & 0xFF) * 0x101 + }; +} + +static void +drwl_setscheme(Drwl *drwl, uint32_t *scm) +{ + if (drwl) + drwl->scheme = scm; +} + +static inline int +drwl_stride(unsigned int width) +{ + return (((PIXMAN_FORMAT_BPP(PIXMAN_a8r8g8b8) * width + 7) / 8 + 4 - 1) & -4); +} + +/* + * Caller must call drwl_finish_drawing when finished drawing. + * Parameter stride can be calculated using drwl_stride. + */ +static void +drwl_prepare_drawing(Drwl *drwl, unsigned int w, unsigned int h, + uint32_t *bits, int stride) +{ + pixman_region32_t clip; + + if (!drwl) + return; + + drwl->pix = pixman_image_create_bits_no_clear( + PIXMAN_a8r8g8b8, w, h, bits, stride); + pixman_region32_init_rect(&clip, 0, 0, w, h); + pixman_image_set_clip_region32(drwl->pix, &clip); + pixman_region32_fini(&clip); +} + +static void +drwl_rect(Drwl *drwl, + int x, int y, unsigned int w, unsigned int h, + int filled, int invert) +{ + pixman_color_t clr; + if (!drwl || !drwl->scheme || !drwl->pix) + return; + + clr = convert_color(drwl->scheme[invert ? ColBg : ColFg]); + if (filled) + pixman_image_fill_rectangles(PIXMAN_OP_SRC, drwl->pix, &clr, 1, + &(pixman_rectangle16_t){x, y, w, h}); + else + pixman_image_fill_rectangles(PIXMAN_OP_SRC, drwl->pix, &clr, 4, + (pixman_rectangle16_t[4]){ + { x, y, w, 1 }, + { x, y + h - 1, w, 1 }, + { x, y, 1, h }, + { x + w - 1, y, 1, h }}); +} + +static int +drwl_text(Drwl *drwl, + int x, int y, unsigned int w, unsigned int h, + unsigned int lpad, const char *text, int invert) +{ + int ty; + int utf8charlen, render = x || y || w || h; + long x_kern; + uint32_t cp = 0, last_cp = 0; + pixman_color_t clr; + pixman_image_t *fg_pix = NULL; + int noellipsis = 0; + const struct fcft_glyph *glyph, *eg; + int fcft_subpixel_mode = FCFT_SUBPIXEL_DEFAULT; + + if (!drwl || (render && (!drwl->scheme || !w || !drwl->pix)) || !text || !drwl->font) + return 0; + + if (!render) { + w = invert ? invert : ~invert; + } else { + clr = convert_color(drwl->scheme[invert ? ColBg : ColFg]); + fg_pix = pixman_image_create_solid_fill(&clr); + + drwl_rect(drwl, x, y, w, h, 1, !invert); + + x += lpad; + w -= lpad; + } + + if (render && (drwl->scheme[ColBg] & 0xFF) != 0xFF) + fcft_subpixel_mode = FCFT_SUBPIXEL_NONE; + + // U+2026 == … + eg = fcft_rasterize_char_utf32(drwl->font, 0x2026, fcft_subpixel_mode); + + while (*text) { + utf8charlen = utf8decode(text, &cp); + + glyph = fcft_rasterize_char_utf32(drwl->font, cp, fcft_subpixel_mode); + if (!glyph) + continue; + + x_kern = 0; + if (last_cp) + fcft_kerning(drwl->font, last_cp, cp, &x_kern, NULL); + last_cp = cp; + + ty = y + (h - drwl->font->height) / 2 + drwl->font->ascent; + + /* draw ellipsis if remaining text doesn't fit */ + if (!noellipsis && x_kern + glyph->advance.x + eg->advance.x > w && *(text + 1) != '\0') { + if (drwl_text(drwl, 0, 0, 0, 0, 0, text, 0) + - glyph->advance.x < eg->advance.x) { + noellipsis = 1; + } else { + w -= eg->advance.x; + pixman_image_composite32( + PIXMAN_OP_OVER, fg_pix, eg->pix, drwl->pix, 0, 0, 0, 0, + x + eg->x, ty - eg->y, eg->width, eg->height); + } + } + + if ((x_kern + glyph->advance.x) > w) + break; + + x += x_kern; + + if (render && pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) + // pre-rendered glyphs (eg. emoji) + pixman_image_composite32( + PIXMAN_OP_OVER, glyph->pix, NULL, drwl->pix, 0, 0, 0, 0, + x + glyph->x, ty - glyph->y, glyph->width, glyph->height); + else if (render) + pixman_image_composite32( + PIXMAN_OP_OVER, fg_pix, glyph->pix, drwl->pix, 0, 0, 0, 0, + x + glyph->x, ty - glyph->y, glyph->width, glyph->height); + + text += utf8charlen; + x += glyph->advance.x; + w -= glyph->advance.x; + } + + if (render) + pixman_image_unref(fg_pix); + + return x + (render ? w : 0); +} + +static unsigned int +drwl_font_getwidth(Drwl *drwl, const char *text) +{ + if (!drwl || !drwl->font || !text) + return 0; + return drwl_text(drwl, 0, 0, 0, 0, 0, text, 0); +} + +static unsigned int +drwl_font_getwidth_clamp(Drwl *drwl, const char *text, unsigned int n) +{ + unsigned int tmp = 0; + if (drwl && drwl->font && text && n) + tmp = drwl_text(drwl, 0, 0, 0, 0, 0, text, n); + return tmp < n ? tmp : n; +} + +static void +drwl_finish_drawing(Drwl *drwl) +{ + if (drwl && drwl->pix) + pixman_image_unref(drwl->pix); +} + +static void +drwl_destroy(Drwl *drwl) +{ + if (drwl->pix) + pixman_image_unref(drwl->pix); + if (drwl->font) + drwl_destroy_font(drwl->font); + free(drwl); +} + +static void +drwl_fini(void) +{ + fcft_fini(); +} diff --git a/mew-run b/mew-run new file mode 100755 index 0000000..5297387 --- /dev/null +++ b/mew-run @@ -0,0 +1,35 @@ +#!/bin/sh + +cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}" +cache="$cachedir/mew_run" + +[ -d "$cachedir" ] || mkdir -p "$cachedir" + +uptodate() { + [ -f "$cache" ] || return 1 + IFS=: + for path in $PATH; do + # non-POSIX + test "$path" -nt "$cache" && return 1 + done + return 0 +} + +bins() { + IFS=: + for path in $PATH; do + for bin in "$path"/*; do + [ -x "$bin" ] && echo "${bin##*/}" + done + done +} + +path() { + if uptodate; then + cat "$cache" + else + bins | sort -u | tee "$cache" + fi +} + +path | mew "$@" | ${SHELL:-"/bin/sh"} & diff --git a/mew.1 b/mew.1 new file mode 100644 index 0000000..81682ac --- /dev/null +++ b/mew.1 @@ -0,0 +1,152 @@ +.TH EMENU 1 mew\-VERSION +.SH NAME +mew \- menu for wayland +.SH SYNOPSIS +.B mew +.RB [ \-biv ] +.RB [ \-l +.IR lines ] +.RB [ \-p +.IR prompt ] +.RB [ \-f +.IR font ] +.RB [ \-nb +.IR color ] +.RB [ \-nf +.IR color ] +.RB [ \-sb +.IR color ] +.RB [ \-sf +.IR color ] +.P +.BR mew-run " ..." +.SH DESCRIPTION +.B mew +is an simple dynamic menu for Wayland that reads a list of newline\-separated items +from stdin. When the user selects an item and presses Return, their choice is printed +to stdout and mew terminates. Entering text will narrow the items to those +matching the tokens in the input. +.P +.B mew-run +is a script spawned by the compositor that lists programs in the user's $PATH and +runs the result in their $SHELL. +.SH OPTIONS +.TP +.B \-b +appear at the bottom of the screen. +.TP +.B \-i +matches menu items case insensitively. +.TP +.BI \-l " lines" +lists item vertically, with the given number of lines. +.TP +.BI \-p " prompt" +defines the prompt to be displayed to the left of the input field. +.TP +.BI \-f " font" +defines the font. +.TP +.BI \-nb " color" +defines the normal background color of the format: +.IR #RRGGBB[FF] +.TP +.BI \-nf " color" +defines the normal foreground color of the format: +.IR #RRGGBB[FF] +.TP +.BI \-sb " color" +defines the selected background color of the format: +.IR #RRGGBB[FF] +.TP +.BI \-sf " color" +defines the selected foreground color of the format: +.IR #RRGGBB[FF] +.TP +.B \-v +prints version information to stdout, then exits. +.SH USAGE +mew is completely controlled by the keyboard. Items are selected using the +arrow keys, page up, page down, home, and end. +.TP +.B Tab +Copy the selected item to the input field. +.TP +.B Return +Confirm selection. Prints the selected item to stdout and exits, returning +success. +.TP +.B Ctrl-Return +Confirm selection. Prints the selected item to stdout and continues. +.TP +.B Shift\-Return +Confirm input. Prints the input text to stdout and exits, returning success. +.TP +.B Escape +Exit without selecting an item, returning failure. +.TP +.B Ctrl-Left +Move cursor to the start of the current word +.TP +.B Ctrl-Right +Move cursor to the end of the current word +.TP +.B C\-a +Home +.TP +.B C\-b +Left +.TP +.B C\-c +Escape +.TP +.B C\-d +Delete +.TP +.B C\-e +End +.TP +.B C\-f +Right +.TP +.B C\-g +Escape +.TP +.B C\-h +Backspace +.TP +.B C\-i +Tab +.TP +.B C\-j +Return +.TP +.B C\-J +Shift-Return +.TP +.B C\-k +Delete line right +.TP +.B C\-m +Return +.TP +.B C\-M +Shift-Return +.TP +.B C\-n +Down +.TP +.B C\-p +Up +.TP +.B C\-u +Delete line left +.TP +.B C\-w +Delete word left +.TP +.B C\-y +Paste from Wayland clipboard +.TP +.B C\-Y +Paste from Wayland clipboard diff --git a/mew.c b/mew.c new file mode 100644 index 0000000..4c59f80 --- /dev/null +++ b/mew.c @@ -0,0 +1,921 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) +#define LENGTH(X) (sizeof (X) / sizeof (X)[0]) + +#include "drwl.h" +#include "poolbuf.h" +#include "wlr-layer-shell-unstable-v1-protocol.h" + +#define TEXTW(X) (drwl_font_getwidth(drw, (X)) + lrpad) + +enum { SchemeNorm, SchemeSel, SchemeOut }; /* color schemes */ + +struct item { + char *text; + struct item *left, *right; + int out; +}; + +static struct { + struct wl_keyboard *wl_keyboard; + struct xkb_context *xkb_context; + struct xkb_keymap *xkb_keymap; + struct xkb_state *xkb_state; + + int repeat_delay; + int repeat_period; + int repeat_timer; + enum wl_keyboard_key_state repeat_key_state; + xkb_keysym_t repeat_sym; + + int ctrl; + int shift; +} kbd; + +static char text[BUFSIZ] = ""; +static int bh, mw, mh; +static int inputw = 0, promptw; +static int32_t scale = 1; +static int lrpad; /* sum of left and right padding */ +static size_t cursor; +static struct item *items = NULL; +static struct item *matches, *matchend; +static struct item *prev, *curr, *next, *sel; +static int running = 1; + +static struct wl_display *display; +static struct wl_compositor *compositor; +static struct wl_seat *seat; +static struct wl_shm *shm; +static struct wl_data_device_manager *data_device_manager; +static struct wl_data_device *data_device; +static struct wl_data_offer *data_offer; +static struct zwlr_layer_shell_v1 *layer_shell; +static struct zwlr_layer_surface_v1 *layer_surface; +static struct wl_surface *surface; +static struct wl_registry *registry; +static Drwl *drw; + +#include "config.h" + +static int (*fstrncmp)(const char *, const char *, size_t) = strncmp; +static char *(*fstrstr)(const char *, const char *) = strstr; + +static void +noop() +{ + /* Space intentionally left blank */ +} + +static void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(EXIT_FAILURE); +} + +static void +parse_color(uint32_t *dest, const char *src) +{ + int len; + + if (src[0] == '#') + src++; + len = strlen(src); + if (len != 6 && len != 8) + die("bad color: %s", src); + + *dest = strtoul(src, NULL, 16); + if (len == 6) + *dest = (*dest << 8) | 0xFF; +} + +static void +loadfonts(void) +{ + char fontattrs[12]; + + drwl_destroy_font(drw->font); + snprintf(fontattrs, sizeof(fontattrs), "dpi=%d", 96 * scale); + if (!(drwl_load_font(drw, LENGTH(fonts), fonts, fontattrs))) + die("no fonts could be loaded"); + + lrpad = drw->font->height; + bh = drw->font->height + 2; + lines = MAX(lines, 0); + mh = (lines + 1) * bh; + promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0; +} + +static unsigned int +textw_clamp(const char *str, unsigned int n) +{ + unsigned int w = drwl_font_getwidth_clamp(drw, str, n) + lrpad; + return MIN(w, n); +} + +static void +appenditem(struct item *item, struct item **list, struct item **last) +{ + if (*last) + (*last)->right = item; + else + *list = item; + + item->left = *last; + item->right = NULL; + *last = item; +} + +static void +calcoffsets(void) +{ + int i, n; + + if (lines > 0) + n = lines * bh; + else + n = mw - (promptw + inputw + TEXTW("<") + TEXTW(">")); + /* calculate which items will begin the next page and previous page */ + for (i = 0, next = curr; next; next = next->right) + if ((i += (lines > 0) ? bh : textw_clamp(next->text, n)) > n) + break; + for (i = 0, prev = curr; prev && prev->left; prev = prev->left) + if ((i += (lines > 0) ? bh : textw_clamp(prev->left->text, n)) > n) + break; +} + +static void +cleanup(void) +{ + size_t i; + + for (i = 0; items && items[i].text; ++i) + free(items[i].text); + free(items); + + drwl_destroy(drw); + drwl_fini(); + + wl_data_device_release(data_device); + zwlr_layer_shell_v1_destroy(layer_shell); + wl_shm_destroy(shm); + wl_compositor_destroy(compositor); + wl_registry_destroy(registry); + wl_display_disconnect(display); +} + +static char * +cistrstr(const char *h, const char *n) +{ + size_t i; + + if (!n[0]) + return (char *)h; + + for (; *h; ++h) { + for (i = 0; n[i] && tolower((unsigned char)n[i]) == + tolower((unsigned char)h[i]); ++i) + ; + if (n[i] == '\0') + return (char *)h; + } + return NULL; +} + +static int +drawitem(struct item *item, int x, int y, int w) +{ + if (item == sel) + drwl_setscheme(drw, colors[SchemeSel]); + else if (item->out) + drwl_setscheme(drw, colors[SchemeOut]); + else + drwl_setscheme(drw, colors[SchemeNorm]); + + return drwl_text(drw, x, y, w, bh, lrpad / 2, item->text, 0); +} + +static void +drawmenu(void) +{ + unsigned int curpos; + struct item *item; + int x = 0, y = 0, w; + PoolBuf *buf; + + if (!(buf = poolbuf_create(shm, mw, mh))) + die("poolbuf_create:"); + + drwl_prepare_drawing(drw, mw, mh, buf->data, buf->stride); + + drwl_setscheme(drw, colors[SchemeNorm]); + drwl_rect(drw, 0, 0, mw, mh, 1, 1); + + if (prompt && *prompt) { + drwl_setscheme(drw, colors[SchemeSel]); + x = drwl_text(drw, x, 0, promptw, bh, lrpad / 2, prompt, 0); + } + /* draw input field */ + w = (lines > 0 || !matches) ? mw - x : inputw; + drwl_setscheme(drw, colors[SchemeNorm]); + drwl_text(drw, x, 0, w, bh, lrpad / 2, text, 0); + + curpos = TEXTW(text) - TEXTW(&text[cursor]); + if ((curpos += lrpad / 2 - 1) < w) { + drwl_setscheme(drw, colors[SchemeNorm]); + drwl_rect(drw, x + curpos, 2, 2, bh - 4, 1, 0); + } + + if (lines > 0) { + /* draw vertical list */ + for (item = curr; item != next; item = item->right) + drawitem(item, x, y += bh, mw - x); + } else if (matches) { + /* draw horizontal list */ + x += inputw; + w = TEXTW("<"); + if (curr->left) { + drwl_setscheme(drw, colors[SchemeNorm]); + drwl_text(drw, x, 0, w, bh, lrpad / 2, "<", 0); + } + x += w; + for (item = curr; item != next; item = item->right) + x = drawitem(item, x, 0, textw_clamp(item->text, mw - x - TEXTW(">"))); + if (next) { + w = TEXTW(">"); + drwl_setscheme(drw, colors[SchemeNorm]); + drwl_text(drw, mw - w, 0, w, bh, lrpad / 2, ">", 0); + } + } + + drwl_finish_drawing(drw); + wl_surface_set_buffer_scale(surface, scale); + wl_surface_attach(surface, buf->wl_buf, 0, 0); + wl_surface_damage_buffer(surface, 0, 0, mw, mh); + poolbuf_destroy(buf); + wl_surface_commit(surface); +} + +static void +match(void) +{ + static char **tokv = NULL; + static int tokn = 0; + + char buf[sizeof text], *s; + int i, tokc = 0; + size_t len, textsize; + struct item *item, *lprefix, *lsubstr, *prefixend, *substrend; + + strcpy(buf, text); + /* separate input text into tokens to be matched individually */ + for (s = strtok(buf, " "); s; tokv[tokc - 1] = s, s = strtok(NULL, " ")) + if (++tokc > tokn && !(tokv = realloc(tokv, ++tokn * sizeof *tokv))) + die("cannot realloc %zu bytes:", tokn * sizeof *tokv); + len = tokc ? strlen(tokv[0]) : 0; + + matches = lprefix = lsubstr = matchend = prefixend = substrend = NULL; + textsize = strlen(text) + 1; + for (item = items; item && item->text; item++) { + for (i = 0; i < tokc; i++) + if (!fstrstr(item->text, tokv[i])) + break; + if (i != tokc) /* not all tokens match */ + continue; + /* exact matches go first, then prefixes, then substrings */ + if (!tokc || !fstrncmp(text, item->text, textsize)) + appenditem(item, &matches, &matchend); + else if (!fstrncmp(tokv[0], item->text, len)) + appenditem(item, &lprefix, &prefixend); + else + appenditem(item, &lsubstr, &substrend); + } + if (lprefix) { + if (matches) { + matchend->right = lprefix; + lprefix->left = matchend; + } else + matches = lprefix; + matchend = prefixend; + } + if (lsubstr) { + if (matches) { + matchend->right = lsubstr; + lsubstr->left = matchend; + } else + matches = lsubstr; + matchend = substrend; + } + curr = sel = matches; + calcoffsets(); +} + +static void +insert(const char *str, ssize_t n) +{ + if (strlen(text) + n > sizeof text - 1) + return; + /* move existing text out of the way, insert new text, and update cursor */ + memmove(&text[cursor + n], &text[cursor], sizeof text - cursor - MAX(n, 0)); + if (n > 0) + memcpy(&text[cursor], str, n); + cursor += n; + match(); +} + +static size_t +nextrune(int inc) +{ + ssize_t n; + + /* return location of next utf8 rune in the given direction (+1 or -1) */ + for (n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc) + ; + return n; +} + +static void +movewordedge(int dir) +{ + if (dir < 0) { /* move cursor to the start of the word*/ + while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) + cursor = nextrune(-1); + while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) + cursor = nextrune(-1); + } else { /* move cursor to the end of the word */ + while (text[cursor] && strchr(worddelimiters, text[cursor])) + cursor = nextrune(+1); + while (text[cursor] && !strchr(worddelimiters, text[cursor])) + cursor = nextrune(+1); + } +} + +static void +paste(void) +{ + int fds[2]; + ssize_t n; + char buf[1024]; + + if (!data_offer) + return; + + if (pipe(fds) < 0) + die("pipe"); + + wl_data_offer_receive(data_offer, "text/plain", fds[1]); + close(fds[1]); + + wl_display_roundtrip(display); + + for (;;) { + n = read(fds[0], buf, sizeof(buf)); + if (n <= 0) + break; + insert(buf, n); + } + close(fds[0]); + + wl_data_offer_destroy(data_offer); +} + +static void +keyboard_keypress(enum wl_keyboard_key_state state, xkb_keysym_t sym, int ctrl, int shift) +{ + char buf[8]; + + 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_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_BackSpace; break; + case XKB_KEY_g: 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: /* fallthrough */ + case XKB_KEY_J: /* fallthrough */ + case XKB_KEY_m: /* fallthrough */ + case XKB_KEY_M: sym = XKB_KEY_Return; break; + case XKB_KEY_n: sym = XKB_KEY_Right; break; + case XKB_KEY_p: sym = XKB_KEY_Up; break; + + case XKB_KEY_k: /* delete right */ + text[cursor] = '\0'; + match(); + goto draw; + case XKB_KEY_u: /* delete left */ + insert(NULL, 0 - cursor); + goto draw; + case XKB_KEY_w: /* delete word */ + while (cursor > 0 && strchr(worddelimiters, text[nextrune(-1)])) + insert(NULL, nextrune(-1) - cursor); + while (cursor > 0 && !strchr(worddelimiters, text[nextrune(-1)])) + insert(NULL, nextrune(-1) - cursor); + goto draw; + case XKB_KEY_y: /* paste selection */ + case XKB_KEY_Y: + paste(); + goto draw; + case XKB_KEY_Left: + case XKB_KEY_KP_Left: + movewordedge(-1); + goto draw; + case XKB_KEY_Right: + case XKB_KEY_KP_Right: + movewordedge(+1); + goto draw; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + break; + case XKB_KEY_bracketleft: + cleanup(); + exit(EXIT_FAILURE); + } + } + switch (sym) { + case XKB_KEY_Delete: + case XKB_KEY_KP_Delete: + if (text[cursor] == '\0') + return; + cursor = nextrune(+1); + /* fallthrough */ + case XKB_KEY_BackSpace: + if (cursor == 0) + return; + insert(NULL, nextrune(-1) - cursor); + break; + case XKB_KEY_End: + case XKB_KEY_KP_End: + if (text[cursor] != '\0') { + cursor = strlen(text); + break; + } + if (next) { + /* jump to end of list and position items in reverse */ + curr = matchend; + calcoffsets(); + curr = prev; + calcoffsets(); + while (next && (curr = curr->right)) + calcoffsets(); + } + sel = matchend; + break; + case XKB_KEY_Escape: + cleanup(); + exit(EXIT_FAILURE); + case XKB_KEY_Home: + case XKB_KEY_KP_Home: + if (sel == matches) { + cursor = 0; + break; + } + sel = curr = matches; + calcoffsets(); + break; + case XKB_KEY_Left: + case XKB_KEY_KP_Left: + if (cursor > 0 && (!sel || !sel->left || lines > 0)) { + cursor = nextrune(-1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XKB_KEY_Up: + case XKB_KEY_KP_Up: + if (sel && sel->left && (sel = sel->left)->right == curr) { + curr = prev; + calcoffsets(); + } + break; + case XKB_KEY_Next: + case XKB_KEY_KP_Next: + if (!next) + return; + sel = curr = next; + calcoffsets(); + break; + case XKB_KEY_Prior: + case XKB_KEY_KP_Prior: + if (!prev) + return; + sel = curr = prev; + calcoffsets(); + break; + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + puts((sel && !shift) ? sel->text : text); + if (!ctrl) + running = 0; + return; + case XKB_KEY_Right: + case XKB_KEY_KP_Right: + if (text[cursor] != '\0') { + cursor = nextrune(+1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XKB_KEY_Down: + case XKB_KEY_KP_Down: + if (sel && sel->right && (sel = sel->right) == next) { + curr = next; + calcoffsets(); + } + break; + case XKB_KEY_Tab: + if (!sel) + return; + cursor = strnlen(sel->text, sizeof text - 1); + memcpy(text, sel->text, cursor); + text[cursor] = '\0'; + match(); + break; + default: + if (xkb_keysym_to_utf8(sym, buf, 8)) + insert(buf, strnlen(buf, 8)); + } +draw: + drawmenu(); +} + +static void +keyboard_handle_keymap(void *data, struct wl_keyboard *wl_keyboard, + uint32_t format, int32_t fd, uint32_t size) +{ + char *map_shm; + + if (format != WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1) + die("unknown keymap"); + + map_shm = mmap(NULL, size, PROT_READ, MAP_SHARED, fd, 0); + if (map_shm == MAP_FAILED) + die("mmap:"); + + kbd.xkb_keymap = xkb_keymap_new_from_string(kbd.xkb_context, map_shm, + XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS); + munmap(map_shm, size); + close(fd); + + kbd.xkb_state = xkb_state_new(kbd.xkb_keymap); +} + +static void +keyboard_handle_key(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t time, uint32_t key, uint32_t _key_state) +{ + struct itimerspec spec = { 0 }; + enum wl_keyboard_key_state key_state = _key_state; + xkb_keysym_t sym = xkb_state_key_get_one_sym(kbd.xkb_state, key + 8); + + keyboard_keypress(key_state, sym, kbd.ctrl, kbd.shift); + + if (key_state == WL_KEYBOARD_KEY_STATE_PRESSED && kbd.repeat_period >= 0) { + kbd.repeat_key_state = key_state; + kbd.repeat_sym = sym; + spec.it_value.tv_sec = kbd.repeat_delay / 1000; + spec.it_value.tv_nsec = (kbd.repeat_delay % 1000) * 1000000l; + } + timerfd_settime(kbd.repeat_timer, 0, &spec, NULL); +} + +static void +keyboard_handle_modifiers(void *data, struct wl_keyboard *wl_keyboard, + uint32_t serial, uint32_t mods_depressed, + uint32_t mods_latched, uint32_t mods_locked, uint32_t group) +{ + xkb_state_update_mask(kbd.xkb_state, + mods_depressed, mods_latched, mods_locked, 0, 0, group); + kbd.ctrl = xkb_state_mod_name_is_active(kbd.xkb_state, + XKB_MOD_NAME_CTRL, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); + kbd.shift = xkb_state_mod_name_is_active(kbd.xkb_state, + XKB_MOD_NAME_SHIFT, + XKB_STATE_MODS_DEPRESSED | XKB_STATE_MODS_LATCHED); +} + +static void +keyboard_repeat(void) +{ + struct itimerspec spec = { 0 }; + + keyboard_keypress(kbd.repeat_key_state, kbd.repeat_sym, kbd.ctrl, kbd.shift); + + spec.it_value.tv_sec = kbd.repeat_period / 1000; + spec.it_value.tv_nsec = (kbd.repeat_period % 1000) * 1000000l; + timerfd_settime(kbd.repeat_timer, 0, &spec, NULL); +} + +static void +keyboard_handle_repeat_info(void *data, struct wl_keyboard *wl_keyboard, + int32_t rate, int32_t delay) +{ + kbd.repeat_delay = delay; + kbd.repeat_period = rate >= 0 ? 1000 / rate : -1; +} + +static const struct wl_keyboard_listener keyboard_listener = { + .keymap = keyboard_handle_keymap, + .enter = noop, + .leave = noop, + .key = keyboard_handle_key, + .modifiers = keyboard_handle_modifiers, + .repeat_info = keyboard_handle_repeat_info, +}; + +static void +layer_surface_handle_configure(void *data, struct zwlr_layer_surface_v1 *surface, + uint32_t serial, uint32_t width, uint32_t height) +{ + mw = width * scale; + mh = height * scale; + inputw = mw / 3; /* input width: ~33% of monitor width */ + zwlr_layer_surface_v1_ack_configure(surface, serial); + drawmenu(); +} + +static void +layer_surface_handle_closed(void *data, struct zwlr_layer_surface_v1 *surface) +{ + running = 0; +} + +static const struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_handle_configure, + .closed = layer_surface_handle_closed, +}; + +static void +surface_handle_preferred_scale(void *data, + struct wl_surface *wl_surface, int32_t factor) +{ + scale = factor; + loadfonts(); + zwlr_layer_surface_v1_set_size(layer_surface, 0, mh / scale); +} + +static const struct wl_surface_listener surface_listener = { + .enter = noop, + .leave = noop, + .preferred_buffer_scale = surface_handle_preferred_scale, + .preferred_buffer_transform = noop, +}; + +static void +data_device_handle_selection(void *data, struct wl_data_device *data_device, + struct wl_data_offer *_data_offer) +{ + data_offer = _data_offer; +} + +static const struct wl_data_device_listener data_device_listener = { + .data_offer = noop, + .selection = data_device_handle_selection, +}; + +static void +seat_handle_capabilities(void *data, struct wl_seat *wl_seat, enum wl_seat_capability caps) +{ + if (!(caps & WL_SEAT_CAPABILITY_KEYBOARD)) + return; + + kbd.wl_keyboard = wl_seat_get_keyboard(seat); + if (!(kbd.xkb_context = xkb_context_new(XKB_CONTEXT_NO_FLAGS))) + die("xkb_context_new failed"); + if ((kbd.repeat_timer = timerfd_create(CLOCK_MONOTONIC, 0)) < 0) + die("timerfd_create:"); + wl_keyboard_add_listener(kbd.wl_keyboard, &keyboard_listener, NULL); +} + +static const struct wl_seat_listener seat_listener = { + .capabilities = seat_handle_capabilities, + .name = noop, +}; + +static void +registry_handle_global(void *data, struct wl_registry *registry, + uint32_t name, const char *interface, uint32_t version) +{ + if (!strcmp(interface, wl_compositor_interface.name)) + compositor = wl_registry_bind(registry, name, &wl_compositor_interface, 6); + else if (!strcmp(interface, wl_shm_interface.name)) + shm = wl_registry_bind(registry, name, &wl_shm_interface, 1); + else if (!strcmp(interface, zwlr_layer_shell_v1_interface.name)) + layer_shell = wl_registry_bind(registry, name, + &zwlr_layer_shell_v1_interface, 1); + else if (!strcmp(interface, wl_data_device_manager_interface.name)) + data_device_manager = wl_registry_bind(registry, name, + &wl_data_device_manager_interface, 3); + else if (!strcmp(interface, wl_seat_interface.name)) { + seat = wl_registry_bind (registry, name, &wl_seat_interface, 4); + wl_seat_add_listener(seat, &seat_listener, NULL); + } +} + +static const struct wl_registry_listener registry_listener = { + .global = registry_handle_global, + .global_remove = noop, +}; + +static void +readstdin(void) +{ + char *line = NULL; + size_t i, itemsiz = 0, linesiz = 0; + ssize_t len; + + /* read each line from stdin and add it to the item list */ + for (i = 0; (len = getline(&line, &linesiz, stdin)) != -1; i++) { + if (i + 1 >= itemsiz) { + itemsiz += 256; + if (!(items = realloc(items, itemsiz * sizeof(*items)))) + die("cannot realloc %zu bytes:", itemsiz * sizeof(*items)); + } + if (line[len - 1] == '\n') + line[len - 1] = '\0'; + if (!(items[i].text = strdup(line))) + die("strdup:"); + + items[i].out = 0; + } + free(line); + if (items) + items[i].text = NULL; + lines = MIN(lines, i); +} + +static void +run(void) +{ + struct pollfd pfds[] = { + { wl_display_get_fd(display), POLLIN }, + { kbd.repeat_timer, POLLIN }, + }; + + match(); + drawmenu(); + + while (running) { + if (wl_display_prepare_read(display) < 0) + if (wl_display_dispatch_pending(display) < 0) + die("wl_display_dispatch_pending:"); + + if (wl_display_flush(display) < 0) + die("wl_display_flush:"); + + if (poll(pfds, LENGTH(pfds), -1) < 0) { + wl_display_cancel_read(display); + die("poll:"); + } + + if (pfds[1].revents & POLLIN) + keyboard_repeat(); + + if (!(pfds[0].revents & POLLIN)) { + wl_display_cancel_read(display); + continue; + } + + if (wl_display_read_events(display) < 0) + die("wl_display_read_events:"); + if (wl_display_dispatch_pending(display) < 0) + die("wl_display_dispatch_pending:"); + } +} + +static void +setup(void) +{ + if (!(display = wl_display_connect(NULL))) + die("failed to connect to wayland"); + + registry = wl_display_get_registry(display); + wl_registry_add_listener(registry, ®istry_listener, NULL); + wl_display_roundtrip(display); + + if (!compositor) + die("wl_compositor not available"); + if (!shm) + die("wl_shm not available"); + if (!layer_shell) + die("layer_shell not available"); + if (!data_device_manager) + die("data_device_manager not available"); + + data_device = wl_data_device_manager_get_data_device( + data_device_manager, seat); + wl_data_device_add_listener(data_device, &data_device_listener, NULL); + + drwl_init(); + if (!(drw = drwl_create())) + die("cannot create drwl drawing context"); + loadfonts(); + + match(); + + surface = wl_compositor_create_surface(compositor); + wl_surface_add_listener(surface, &surface_listener, NULL); + + layer_surface = zwlr_layer_shell_v1_get_layer_surface(layer_shell, + surface, NULL, ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, "mew"); + zwlr_layer_surface_v1_set_size(layer_surface, 0, mh); + zwlr_layer_surface_v1_set_anchor(layer_surface, + (top ? ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP : ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM ) | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT); + zwlr_layer_surface_v1_set_exclusive_zone(layer_surface, -1); + zwlr_layer_surface_v1_set_keyboard_interactivity(layer_surface, true); + zwlr_layer_surface_v1_add_listener(layer_surface, + &layer_surface_listener, NULL); + + wl_surface_commit(surface); + wl_display_roundtrip(display); +} + +static void +usage(void) +{ + die("usage: mew [-biv] [-l lines] [-p prompt] [-f font]\n" + " [-nb color] [-nf color] [-sb color] [-sf color]"); +} + +int +main(int argc, char *argv[]) +{ + int i; + + for (i = 1; i < argc; i++) { + /* these options take no arguments */ + if (!strcmp(argv[i], "-v")) { + puts("mew-"VERSION); + exit(0); + } else if (!strcmp(argv[i], "-b")) + top = 0; + else if (!strcmp(argv[i], "-i")) { + fstrncmp = strncasecmp; + fstrstr = cistrstr; + } else if (i + 1 == argc) + usage(); + else if (!strcmp(argv[i], "-l")) + lines = atoi(argv[++i]); + else if (!strcmp(argv[i], "-p")) + prompt = argv[++i]; + else if (!strcmp(argv[i], "-f")) + fonts[0] = argv[++i]; + else if (!strcmp(argv[i], "-nb")) + parse_color(&colors[SchemeNorm][ColBg], argv[++i]); + else if (!strcmp(argv[i], "-nf")) + parse_color(&colors[SchemeNorm][ColFg], argv[++i]); + else if (!strcmp(argv[i], "-sb")) + parse_color(&colors[SchemeSel][ColBg], argv[++i]); + else if (!strcmp(argv[i], "-sf")) + parse_color(&colors[SchemeSel][ColFg], argv[++i]); + else + usage(); + } + + readstdin(); +#ifdef __OpenBSD__ + if (pledge("stdio rpath", NULL) == -1) + die("pledge"); +#endif + setup(); + run(); + cleanup(); + + return EXIT_SUCCESS; +} diff --git a/poolbuf.h b/poolbuf.h new file mode 100644 index 0000000..3cf9e7e --- /dev/null +++ b/poolbuf.h @@ -0,0 +1,112 @@ +/* + * poolbuf - https://codeberg.org/sewn/drwl + * See LICENSE file for copyright and license details. + */ +#include +#include +#include +#include +#include +#include + +#include "drwl.h" + +typedef struct { + struct wl_buffer *wl_buf; + int32_t stride; + int32_t size; + void *data; +} PoolBuf; + +#if defined(__linux__) || defined(___FreeBSD__) +#ifdef __linux__ +#include +#endif +#define __POOLBUF_HAS_MEMFD_CREATE +int memfd_create(const char *, unsigned); +#else +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 +create_shm(void) +{ + char name[] = "/drwl-XXXXXX"; + int retries = 100; + + do { + randname(name + sizeof(name) - 7); + --retries; + 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; +} +#endif + +/* Caller must call poolbuf_destroy after finalizing usage */ +static PoolBuf * +poolbuf_create(struct wl_shm *shm, int32_t width, int32_t height) +{ + int fd; + void *data; + struct wl_shm_pool *shm_pool; + struct wl_buffer *wl_buf; + int32_t stride = drwl_stride(width); + int32_t size = stride * height; + PoolBuf *buf; + +#ifdef __POOLBUF_HAS_MEMFD_CREATE + fd = memfd_create("drwl-shm-buffer-pool", + MFD_CLOEXEC | MFD_ALLOW_SEALING | MFD_NOEXEC_SEAL); +#else + fd = create_shm(); +#endif + if (fd < 0) + return NULL; + + if ((ftruncate(fd, size)) < 0) { + close(fd); + return NULL; + } + + data = mmap(NULL, size, + PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (data == MAP_FAILED) { + close(fd); + return NULL; + } + + shm_pool = wl_shm_create_pool(shm, fd, size); + wl_buf = wl_shm_pool_create_buffer(shm_pool, 0, + width, height, stride, WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(shm_pool); + close(fd); + + buf = calloc(1, sizeof(PoolBuf)); + buf->wl_buf = wl_buf; + buf->stride = stride; + buf->size = size; + buf->data = data; + return buf; +} + +static void +poolbuf_destroy(PoolBuf *buf) +{ + wl_buffer_destroy(buf->wl_buf); + munmap(buf->data, buf->size); + free(buf); +} 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. + + + + + + + + + + + + + + + + +