From c73c22aa56d8df56d7c1c0c039e20c3fc6059eeb Mon Sep 17 00:00:00 2001 From: speedie Date: Fri, 20 Jan 2023 23:17:30 +0100 Subject: [PATCH] Add initial build of spmenu --- LICENSE | 31 + Makefile | 54 ++ README.md | 85 ++ arg.h | 49 + colors.h | 45 + docs/example.Xresources | 54 ++ draw.c | 285 ++++++ draw.h | 57 ++ host.mk | 38 + main.c | 35 + main.h | 12 + options.h | 103 +++ options.mk | 9 + scripts/spmenu_run | 22 + spmenu.c | 1888 +++++++++++++++++++++++++++++++++++++++ toggle.mk | 8 + xresources.h | 90 ++ 17 files changed, 2865 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 arg.h create mode 100644 colors.h create mode 100644 docs/example.Xresources create mode 100644 draw.c create mode 100644 draw.h create mode 100644 host.mk create mode 100644 main.c create mode 100644 main.h create mode 100644 options.h create mode 100644 options.mk create mode 100755 scripts/spmenu_run create mode 100644 spmenu.c create mode 100644 toggle.mk create mode 100644 xresources.h diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a9805fb --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +MIT/X Consortium License + +© 2006-2019 Anselm R Garbe +© 2006-2008 Sander van Dijk +© 2006-2007 Michał Janeczek +© 2007 Kris Maglione +© 2009 Gottox +© 2009 Markus Schnalke +© 2009 Evan Gates +© 2010-2012 Connor Lane Smith +© 2014-2022 Hiltjo Posthuma +© 2015-2019 Quentin Rameau +© 2021-2023 speedie + +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..846870a --- /dev/null +++ b/Makefile @@ -0,0 +1,54 @@ +# spmenu + +# See LICENSE file for copyright and license details. + +include options.mk +include host.mk +include toggle.mk + +SRC = draw.c spmenu.c main.c +OBJ = $(SRC:.c=.o) + +all: options spmenu + +options: + @echo spmenu build options: + @echo "CFLAGS = $(CFLAGS)" + @echo "LDFLAGS = $(LDFLAGS)" + @echo "CC = $(CC)" + +.c.o: + $(CC) -c $(CFLAGS) -g $< + + +$(OBJ): arg.h options.h options.mk draw.h + +spmenu: spmenu.o draw.o main.o + $(CC) -o $@ spmenu.o draw.o main.o $(LDFLAGS) + +clean: + rm -f spmenu $(OBJ) spmenu-$(VERSION).tar.gz + +dist: clean + mkdir -p spmenu-$(VERSION) + cp LICENSE Makefile *.h *.mk *.c scripts/ spmenu-$(VERSION) + tar -cf spmenu-$(VERSION).tar spmenu-$(VERSION) + gzip spmenu-$(VERSION).tar + rm -rf spmenu-$(VERSION) + +install: all + mkdir -p $(DESTDIR)$(PREFIX)/bin + cp -f spmenu scripts/* $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/spmenu* + rm -f *.o + rm -f spmenu + +uninstall: + $(DESTDIR)$(PREFIX)/bin/spmenu*\ + +help: + @echo install: Installs spmenu. You may need to run this as root. + @echo uninstall: Uninstalls spmenu. You may need to run this as root. + @echo help: Displays this help sheet. + +.PHONY: all options clean dist install uninstall help diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b39251 --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# spmenu + +Simply my build of suckless's dmenu + +### What is spmenu? + +spmenu is an X11 menu application based on +[dmenu](https://tools.suckless.org/dmenu) which takes standard input, parses +it, and lets the user choose an option and sends the +selected option to standard output. + +It is designed to integrate well with my [dwm](https://dwm.suckless.org) fork, [speedwm](https://codeberg.org/speedie/speedwm). + +### Special features + +This build of spmenu has some features written for this build. +Of course if you want, this is free software so you can use it in your own build. + +- 256 color support through SGR codes. +- Option to block typing. +- Rewritten arguments, old arguments still work though. +- Border only when centered option +- Hiding each part of the menu + +### Other features + +Note: This is an incomplete list, it's just here to give you an idea of what this build has to offer. +- Pango markup support. +- Alpha transparency +- Pywal/.Xresources support +- Grid +- Colored Emoji/Font support +- Highlighting +- Case-insensitive by default +- Padding; useful with patched dwm with barpadding or speedwm. +- Fuzzy-finding +- Preselect support +- Line-height +- History support + +### Dependencies + +- libX11 +- libXrender +- freetype +- libXinerama + - Can be disabled if you don't want/need multi-monitor support. +- tcc compiler (you can swap it out for GCC by passing CC="gcc" to the `make` command if you want) +- Pango (for drawing fonts) + - If you do not want to use pango, consider my [older spmenu build](https://github.com/speedie-de/dmenu) + +### Installation (most GNU/Linux distributions) + +`emerge dev-vcs/git # Install dev-vcs/git using your favorite package manager` + +`git clone https://codeberg.org/speedie/spmenu` + +`cd spmenu/` + +`make clean install # Run as root.` + +### Installation (Gentoo) + +If you are on Gentoo GNU/Linux, you can add +[my overlay](https://codeberg.org/speedie/speedie-overlay) which includes +`x11-misc/spmenu` as well as other useful packages. + +### .Xresources values + +This build allows you to define .Xresources values to load on startup. See docs/example.Xresources for a list of default values. + +### Scripts + +This build of spmenu should work with all spmenu scripts. [Here](https://codeberg.org/speedie/speedwm-extras) are a few I've written/use: + +### Notes for users of Arch + +This fork of spmenu is compiled using tcc for speed however tcc from the Arch repositories seems to be broken. I'm sure there's a better way to fix this but I just fix it by installing [this package](https://aur.archlinux.org/packages/tcc-ziyao) from the AUR. + +### Notes for GCC users + +If you're compiling with GCC, chances are you're seeing a lot of warnings. +This is because we're compiling with -Ofast. I can't seem to find any issues +with using -Ofast but if it bothers you, you can compile +with -Os or -O2 which don't spit out these warnings. diff --git a/arg.h b/arg.h new file mode 100644 index 0000000..e94e02b --- /dev/null +++ b/arg.h @@ -0,0 +1,49 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + for (brk_ = 0, argv[0]++, argv_ = argv;\ + argv[0][0] && !brk_;\ + argv[0]++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][0];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][1] != '\0')?\ + (&argv[0][1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/colors.h b/colors.h new file mode 100644 index 0000000..85c7713 --- /dev/null +++ b/colors.h @@ -0,0 +1,45 @@ +/* Color options */ + +/* Alpha */ +static const unsigned int alphas[][3] = { + /* fg bg border */ + [SchemeNorm] = { opaque, baralpha, borderalpha }, + [SchemeSel] = { opaque, baralpha, borderalpha }, + [SchemePrompt] = { opaque, baralpha, borderalpha }, + [SchemeNormHighlight] = { opaque, baralpha, borderalpha }, + [SchemeSelHighlight] = { opaque, baralpha, borderalpha }, + [SchemeCaret] = { opaque, baralpha, borderalpha }, + [SchemeNumber] = { opaque, baralpha, borderalpha }, +}; + +/* Colors */ +static const char *colors[SchemeLast][2] = { + /* fg bg */ + [SchemeNorm] = { normfgcolor, normbgcolor }, + [SchemeSel] = { selfgcolor, selbgcolor }, + [SchemePrompt] = { selfgcolor, selbgcolor }, + [SchemeNormHighlight] = { normhlfgcolor, normhlbgcolor }, + [SchemeSelHighlight] = { selhlfgcolor, selhlbgcolor }, + [SchemeCaret] = { caretfgcolor, NULL }, + [SchemeNumber] = { numfgcolor, numbgcolor }, +}; + +/* sgr colors */ +static char *textcolors[] = { + sgrcolor0, + sgrcolor1, + sgrcolor2, + sgrcolor3, + sgrcolor4, + sgrcolor5, + sgrcolor6, + sgrcolor7, + sgrcolor8, + sgrcolor9, + sgrcolor10, + sgrcolor11, + sgrcolor12, + sgrcolor13, + sgrcolor14, + sgrcolor15, +}; diff --git a/docs/example.Xresources b/docs/example.Xresources new file mode 100644 index 0000000..fefb761 --- /dev/null +++ b/docs/example.Xresources @@ -0,0 +1,54 @@ +!! spmenu +!! +!! colors +spmenu.font: DejaVu Sans Mono 8 +spmenu.normfgcolor: #bbbbbb +spmenu.normbgcolor: #222222 +spmenu.selfgcolor: #eeeeee +spmenu.selbgcolor: #005577 +spmenu.normhlfgcolor: #ffffff +spmenu.normhlbgcolor: #000000 +spmenu.selhlfgcolor: #ffffff +spmenu.selhlbgcolor: #000000 +spmenu.outfgcolor: #000000 +spmenu.outbgcolor: #5e81ac +spmenu.caretfgcolor: #222222 +spmenu.sgrcolor0: #000000 +spmenu.sgrcolor1: #7f0000 +spmenu.sgrcolor2: #007f00 +spmenu.sgrcolor3: #7f7f00 +spmenu.sgrcolor4: #00007f +spmenu.sgrcolor5: #7f007f +spmenu.sgrcolor6: #007f7f +spmenu.sgrcolor7: #cccccc +spmenu.sgrcolor8: #333333 +spmenu.sgrcolor9: #ff0000 +spmenu.sgrcolor10: #00ff00 +spmenu.sgrcolor11: #ffff00 +spmenu.sgrcolor12: #0000ff +spmenu.sgrcolor13: #ff00ff +spmenu.sgrcolor14: #00ffff +spmenu.sgrcolor15: #ffffff + +!! misc +spmenu.alpha: 1 +spmenu.bordercentered: 1 +spmenu.borderwidth: 2 +spmenu.lines: 0 +spmenu.columns: 10 +spmenu.lineheight: 5 +spmenu.minlineheight: 5 +spmenu.maxhist: 64 +spmenu.histnodup: 1 +spmenu.casesensitive: 0 +spmenu.menuposition: 1 +spmenu.menupaddingv: 0 +spmenu.menupaddingh: 0 +spmenu.colorprompt: 1 +spmenu.preselected: 0 +spmenu.type: 1 +spmenu.class: spmenu +spmenu.leftarrow: < +spmenu.rightarrow: > + +spmenu.hidematchcount: 0 diff --git a/draw.c b/draw.c new file mode 100644 index 0000000..c83118d --- /dev/null +++ b/draw.c @@ -0,0 +1,285 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include + +#include "draw.h" +#include "main.h" + +Drw * +drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h, Visual *visual, unsigned int depth, Colormap cmap) +{ + Drw *drw = ecalloc(1, sizeof(Drw)); + + drw->dpy = dpy; + drw->screen = screen; + drw->root = root; + drw->w = w; + drw->h = h; + drw->visual = visual; + drw->depth = depth; + drw->cmap = cmap; + drw->drawable = XCreatePixmap(dpy, root, w, h, depth); + drw->gc = XCreateGC(dpy, drw->drawable, 0, NULL); + XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); + + return drw; +} + +void +drw_resize(Drw *drw, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + drw->w = w; + drw->h = h; + if (drw->drawable) + XFreePixmap(drw->dpy, drw->drawable); + drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, drw->depth); +} + +void +drw_free(Drw *drw) +{ + XFreePixmap(drw->dpy, drw->drawable); + XFreeGC(drw->dpy, drw->gc); + drw_font_free(drw->font); + free(drw); +} + +static Fnt * +xfont_create(Drw *drw, const char *fontname) +{ + Fnt *font; + PangoFontMap *fontmap; + PangoContext *context; + PangoFontDescription *desc; + PangoFontMetrics *metrics; + + if (!fontname) { + die("no font specified."); + } + + font = ecalloc(1, sizeof(Fnt)); + font->dpy = drw->dpy; + + fontmap = pango_xft_get_font_map(drw->dpy, drw->screen); + context = pango_font_map_create_context(fontmap); + desc = pango_font_description_from_string(fontname); + font->layout = pango_layout_new(context); + pango_layout_set_font_description(font->layout, desc); + + metrics = pango_context_get_metrics(context, desc, pango_language_from_string ("en-us")); + font->h = pango_font_metrics_get_height(metrics) / PANGO_SCALE; + + pango_font_metrics_unref(metrics); + g_object_unref(context); + + return font; +} + +static void +xfont_free(Fnt *font) +{ + if (!font) + return; + if (font->layout) + g_object_unref(font->layout); + free(font); +} + +Fnt* +drw_font_create(Drw* drw, const char font[]) +{ + Fnt *fnt = NULL; + + if (!drw || !font) + return NULL; + + fnt = xfont_create(drw, font); + + return (drw->font = fnt); +} + +void +drw_font_free(Fnt *font) +{ + if (font) { + xfont_free(font); + } +} + +void +drw_clr_create(Drw *drw, Clr *dest, const char *clrname, unsigned int alpha) +{ + if (!drw || !dest || !clrname) + return; + + if (!XftColorAllocName(drw->dpy, drw->visual, drw->cmap, + clrname, dest)) + die("error, cannot allocate color '%s'", clrname); + + dest->pixel = (dest->pixel & 0x00ffffffU) | (alpha << 24); +} + +/* Wrapper to create color schemes. The caller has to call free(3) on the + * returned color scheme when done using it. */ +Clr * +drw_scm_create(Drw *drw, const char *clrnames[], const unsigned int alphas[], size_t clrcount) +{ + size_t i; + Clr *ret; + + /* need at least two colors for a scheme */ + if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(XftColor)))) + return NULL; + + for (i = 0; i < clrcount; i++) + drw_clr_create(drw, &ret[i], clrnames[i], alphas[i]); + return ret; +} + +void +drw_setscheme(Drw *drw, Clr *scm) +{ + if (drw) + drw->scheme = scm; +} + +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + if (!drw || !drw->scheme) + return; + XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel); + if (filled) + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + else + XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1); +} + +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert, Bool markup) +{ + char buf[1024]; + int ty; + unsigned int ew; + XftDraw *d = NULL; + size_t i, len; + int render = x || y || w || h; + + if (!drw || (render && !drw->scheme) || !text || !drw->font) + return 0; + + if (!render) { + w = ~w; + } else { + XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + d = XftDrawCreate(drw->dpy, drw->drawable, drw->visual, drw->cmap); + x += lpad; + w -= lpad; + } + + len = strlen(text); + + if (len) { + drw_font_getexts(drw->font, text, len, &ew, NULL, markup); + /* shorten text if necessary */ + for (len = MIN(len, sizeof(buf) - 1); len && ew > w; len--) + drw_font_getexts(drw->font, text, len, &ew, NULL, markup); + + if (len) { + memcpy(buf, text, len); + buf[len] = '\0'; + if (len < strlen(text)) + for (i = len; i && i > len - 3; buf[--i] = '.') + ; /* NOP */ + + if (render) { + ty = y + (h - drw->font->h) / 2; + if(markup) + pango_layout_set_markup(drw->font->layout, buf, len); + else + pango_layout_set_text(drw->font->layout, buf, len); + pango_xft_render_layout(d, &drw->scheme[invert ? ColBg : ColFg], + drw->font->layout, x * PANGO_SCALE, ty * PANGO_SCALE); + if(markup) /* clear markup attributes */ + pango_layout_set_attributes(drw->font->layout, NULL); + } + + x += ew; + w -= ew; + } + } + if (d) + XftDrawDestroy(d); + + return x + (render ? w : 0); +} + +void +drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y); + XSync(drw->dpy, False); +} + +unsigned int +drw_font_getwidth(Drw *drw, const char *text, Bool markup) +{ + if (!drw || !drw->font || !text) + return 0; + return drw_text(drw, 0, 0, 0, 0, 0, text, 0, markup); +} + +void +drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h, Bool markup) +{ + if (!font || !text) + return; + + PangoRectangle r; + if(markup) + pango_layout_set_markup(font->layout, text, len); + else + pango_layout_set_text(font->layout, text, len); + pango_layout_get_extents(font->layout, 0, &r); + if(markup) /* clear markup attributes */ + pango_layout_set_attributes(font->layout, NULL); + if (w) + *w = r.width / PANGO_SCALE; + if (h) + *h = font->h; +} + +Cur * +drw_cur_create(Drw *drw, int shape) +{ + Cur *cur; + + if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) + return NULL; + + cur->cursor = XCreateFontCursor(drw->dpy, shape); + + return cur; +} + +void +drw_cur_free(Drw *drw, Cur *cursor) +{ + if (!cursor) + return; + + XFreeCursor(drw->dpy, cursor->cursor); + free(cursor); +} diff --git a/draw.h b/draw.h new file mode 100644 index 0000000..10b467d --- /dev/null +++ b/draw.h @@ -0,0 +1,57 @@ +/* See LICENSE file for copyright and license details. */ + +typedef struct { + Cursor cursor; +} Cur; + +typedef struct Fnt { + Display *dpy; + unsigned int h; + PangoLayout *layout; +} Fnt; + +enum { ColFg, ColBg }; /* Clr scheme index */ +typedef XftColor Clr; + +typedef struct { + unsigned int w, h; + Display *dpy; + int screen; + Window root; + Visual *visual; + unsigned int depth; + Colormap cmap; + Drawable drawable; + GC gc; + Clr *scheme; + Fnt *font; +} Drw; + +/* Drawable abstraction */ +Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h, Visual *visual, unsigned int depth, Colormap cmap); +void drw_resize(Drw *drw, unsigned int w, unsigned int h); +void drw_free(Drw *drw); + +/* Fnt abstraction */ +Fnt *drw_font_create(Drw* drw, const char font[]); +void drw_font_free(Fnt* set); +unsigned int drw_font_getwidth(Drw *drw, const char *text, Bool markup); +void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h, Bool markup); + +/* Colorscheme abstraction */ +void drw_clr_create(Drw *drw, Clr *dest, const char *clrname, const unsigned int alpha); +Clr *drw_scm_create(Drw *drw, const char *clrnames[], const unsigned int alphas[], size_t clrcount); + +/* Cursor abstraction */ +Cur *drw_cur_create(Drw *drw, int shape); +void drw_cur_free(Drw *drw, Cur *cursor); + +/* Drawing context manipulation */ +void drw_setscheme(Drw *drw, Clr *scm); + +/* Drawing functions */ +void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert); +int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert, Bool markup); + +/* Map functions */ +void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); diff --git a/host.mk b/host.mk new file mode 100644 index 0000000..bce2622 --- /dev/null +++ b/host.mk @@ -0,0 +1,38 @@ +# spmenu +# See LICENSE file for copyright details. + +# compiler +CC = tcc + +# paths +PREFIX = /usr + +# library paths +# +# libx11 +X11LIBS = -lX11 +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# freetype +FREETYPELIBS = -lfontconfig -lXft +FREETYPEINC = /usr/include/freetype2 + +# xft +XFT_CONF = xft + +# pango +PANGO_CONF = pango +PANGOXFT_CONF = pangoxft + +# xrender +XRENDERLIBS = -lXrender + +# OpenBSD (uncomment) +#FREETYPEINC = $(X11INC)/freetype2 + +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS) +CFLAGS = -std=c99 -pedantic -Wall -Ofast $(INCS) $(CPPFLAGS) +LDFLAGS = $(LIBS) +INCS = -I$(X11INC) -I$(FREETYPEINC) `pkg-config --cflags $(XFT_CONF) $(PANGO_CONF) $(PANGOXFT_CONF)` +LIBS = -L$(X11LIB) $(X11LIBS) $(XINERAMALIBS) $(FREETYPELIBS) $(XRENDERLIBS) -lm `pkg-config --libs $(XFT_CONF) $(PANGO_CONF) $(PANGOXFT_CONF)` diff --git a/main.c b/main.c new file mode 100644 index 0000000..4c75a23 --- /dev/null +++ b/main.c @@ -0,0 +1,35 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "main.h" + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} + +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(1); +} diff --git a/main.h b/main.h new file mode 100644 index 0000000..531ab25 --- /dev/null +++ b/main.h @@ -0,0 +1,12 @@ +/* See LICENSE file for copyright and license details. */ + +#ifndef MAX +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#endif +#ifndef MIN +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#endif +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) + +void die(const char *fmt, ...); +void *ecalloc(size_t nmemb, size_t size); diff --git a/options.h b/options.h new file mode 100644 index 0000000..e284791 --- /dev/null +++ b/options.h @@ -0,0 +1,103 @@ +/* spmenu + * Below is a configuration file which is technically C source code. + * See the LICENSE file for license details. + */ + +/* spmenu options */ +static char class[] = "spmenu"; /* Class for spmenu */ +static int xresources = 1; /* Enable .Xresources support */ + +/* Window options */ +static int alpha = 1; /* Enable alpha */ +static int menuposition = 1; /* Position of the menu */ +static int menupaddingv = 0; /* Vertical padding of bar (in pixels) */ +static int menupaddingh = 0; /* Horizontal padding of bar (in pixels) */ +static int minwidth = 500; /* Minimum width */ +static int centered = 0; /* Whether or not to center spmenu by default */ + +static int colorsupport = 0; /* Support 256 colors? Otherwise the default 16 colors will be used. */ + +/* Window border options */ +static int borderwidth = 2; /* Width of the border */ +static int bordercentered = 1; /* Draw border only when centered */ + +/* Font options */ +static char font[] = "DejaVu Sans Mono 8"; /* Font to draw text and Pango markup with. */ + +/* Symbol options */ +static char *leftarrow = "<"; /* Left arrow, used to indicate you can move to the left */ +static char *rightarrow = ">"; /* Right arrow, used to indicate you can move to the right */ + +/* Match options */ +static int type = 1; /* Allow typing into spmenu or only allow keybinds. */ +static int casesensitive = 0; /* Case-sensitive by default? (0/1) */ +static int preselected = 0; /* Which line should spmenu preselect? */ +static int fuzzy = 1; /* Whether or not to enable fuzzy matching by default */ + +/* Line options */ +static int lines = 0; /* Default number of lines */ +static int columns = 10; /* Default number of columns */ +static int lineheight = 20; /* Height of each line */ +static int minlineheight = 5; /* Minimum line height */ + +/* History options */ +static unsigned int maxhist = 64; /* Max number of history entries */ +static int histnodup = 1; /* If 0, record repeated histories */ + +/* Prompt options */ +static int colorprompt = 1; /* Color prompt (0/1) */ +static char *prompt = NULL; /* Default prompt, set to NULL (nothing) */ + +/* Hide options */ +static int hidematchcount = 0; /* Hide match count (0/1) */ + +/* Color options + * + * Normal foreground colors + */ +static char normfgcolor[] = "#bbbbbb"; /* Text color for unselected */ +static char normbgcolor[] = "#222222"; /* Background color for unselected */ + +/* Selected foreground colors */ +static char selfgcolor[] = "#eeeeee"; /* Text color for selected */ +static char selbgcolor[] = "#5e81ac"; /* Background color for selected */ + +/* Normal highlight colors */ +static char normhlfgcolor[] = "#ffffff"; /* Text highlight color for unselected */ +static char normhlbgcolor[] = "#000000"; /* Background highlight color for unselected */ + +/* Selected highlight colors */ +static char selhlfgcolor[] = "#ffffff"; /* Text highlight color for selected */ +static char selhlbgcolor[] = "#000000"; /* Background highlight color for selected */ + +/* Match count colors */ +static char numfgcolor[] = "#ffffff"; /* Match count text color */ +static char numbgcolor[] = "#000000"; /* Match count background color */ + +/* Caret colors */ +static char caretfgcolor[] = "#222222"; /* Caret color */ + +/* SGR colors */ +static char sgrcolor0[] = "#000000"; /* SGR color #0 */ +static char sgrcolor1[] = "#7f0000"; /* SGR color #1 */ +static char sgrcolor2[] = "#007f00"; /* SGR color #2 */ +static char sgrcolor3[] = "#7f7f00"; /* SGR color #3 */ +static char sgrcolor4[] = "#00007f"; /* SGR color #4 */ +static char sgrcolor5[] = "#7f007f"; /* SGR color #5 */ +static char sgrcolor6[] = "#007f7f"; /* SGR color #6 */ +static char sgrcolor7[] = "#cccccc"; /* SGR color #7 */ +static char sgrcolor8[] = "#333333"; /* SGR color #8 */ +static char sgrcolor9[] = "#ff0000"; /* SGR color #9 */ +static char sgrcolor10[] = "#00ff00"; /* SGR color #10 */ +static char sgrcolor11[] = "#ffff00"; /* SGR color #11 */ +static char sgrcolor12[] = "#0000ff"; /* SGR color #12 */ +static char sgrcolor13[] = "#ff00ff"; /* SGR color #13 */ +static char sgrcolor14[] = "#00ffff"; /* SGR color #14 */ +static char sgrcolor15[] = "#ffffff"; /* SGR color #15 */ + +/* Alpha options */ +#define baralpha 200 /* Bar alpha */ +#define borderalpha opaque /* Border alpha */ + +/* Misc */ +static char worddelimiters[] = " "; /* Word delimiters, " " is default. */ diff --git a/options.mk b/options.mk new file mode 100644 index 0000000..3ed0d82 --- /dev/null +++ b/options.mk @@ -0,0 +1,9 @@ +# spmenu +# See LICENSE file for copyright details. + +# spmenu version +VERSION = 0.1 + +# includes and libs +INCS = -I$(X11INC) -I$(FREETYPEINC) `pkg-config --cflags xft pango pangoxft` +LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) ${XRENDERLIBS} -lm `pkg-config --libs xft pango pangoxft` diff --git a/scripts/spmenu_run b/scripts/spmenu_run new file mode 100755 index 0000000..f0c5908 --- /dev/null +++ b/scripts/spmenu_run @@ -0,0 +1,22 @@ +#!/bin/sh + +# get path +PATH() { + echo "$PATH" | tr ':' '\n' | uniq | sed 's#$#/#' | # List directories in $PATH + xargs ls -lu --time-style=+%s | # Add atime epoch + awk '/^(-|l)/ { print $6, $7 }' | # Only print timestamp and name + sort -rn | cut -d' ' -f 2 +} + +# Run spmenu and parse it +PARSE() { + DOUT="$(PATH | spmenu -H "${XDG_CACHE_HOME:-$HOME/.cache/}/spmenu_run.hist" "$@")" + + [ "$(printf '%c' "$DOUT")" = "#" ] && RUN_ARG="$TERMINAL" || RUN_ARG="$SHELL" + [ "$RUN_ARG" != "$TERMINAL" ] && printf "%s" "$DOUT" | sed "s/#//g" | ${SHELL:-"/bin/sh"} & + [ "$RUN_ARG" = "$TERMINAL" ] && $TERMINAL -e "$(printf "%s" "$DOUT" | sed "s/#//g")" +} + +PARSE "$@" + +rm -f "$HOME/.cache/spmenu_run" diff --git a/spmenu.c b/spmenu.c new file mode 100644 index 0000000..2c5d36c --- /dev/null +++ b/spmenu.c @@ -0,0 +1,1888 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#ifdef XINERAMA +#include +#endif +#include +#include + +#include "draw.h" +#include "main.h" + +/* macros */ +#define INTERSECT(x,y,w,h,r) (MAX(0, MIN((x)+(w),(r).x_org+(r).width) - MAX((x),(r).x_org)) \ + && MAX(0, MIN((y)+(h),(r).y_org+(r).height) - MAX((y),(r).y_org))) +#define LENGTH(X) (sizeof X / sizeof X[0]) +#define TEXTW(X) (drw_font_getwidth(drw, (X), False) + lrpad) +#define TEXTWM(X) (drw_font_getwidth(drw, (X), True) + lrpad) + +/* number */ +#define NUMBERSMAXDIGITS 100 +#define NUMBERSBUFSIZE (NUMBERSMAXDIGITS * 2) + 1 + +/* alpha */ +#define opaque 0xffU + +/* enums */ +enum { SchemeNorm, + SchemeSel, + SchemePrompt, + SchemeCaret, + SchemeOut, + SchemeNumber, + SchemeNormHighlight, + SchemeSelHighlight, + SchemeLast }; /* color schemes */ + +struct item { + char *text; + struct item *left, *right; + int out; + double distance; +}; + +static char numbers[NUMBERSBUFSIZE] = ""; +static char text[BUFSIZ] = ""; +static char *embed; +static int bh, mw, mh; +static int dmx = 0; /* put spmenu at this x offset */ +static int dmy = 0; /* put spmenu at this y offset (measured from the bottom if menuposition is 0) */ +static unsigned int dmw = 0; /* make spmenu this wide */ +static int inputw = 0, promptw, passwd = 0; +static int lrpad; /* sum of left and right padding */ +static int vp; /* vertical padding for bar */ +static int sp; /* side padding for bar */ +static size_t cursor; +static struct item *items = NULL, *backup_items; +static struct item *matches, *matchend; +static struct item *prev, *curr, *next, *sel; +static int mon = -1, screen; +static int managed = 0; + +static Atom clip, utf8; +static Display *dpy; +static Window root, parentwin, win; +static XIC xic; + +static int useargb = 0; +static Visual *visual; +static int depth; +static Colormap cmap; + +static Drw *drw; +static Clr *scheme[SchemeLast]; +static Clr textclrs[256]; + +static char *histfile; +static char **history; +static size_t histsz, histpos; + +/* Xresources preferences */ +enum resource_type { + STRING = 0, + INTEGER = 1, + FLOAT = 2 +}; +typedef struct { + char *name; + enum resource_type type; + void *dst; +} ResourcePref; + +static void load_xresources(void); +static void resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst); + + +#include "options.h" /* Include user configuration */ +#include "colors.h" /* Include colors */ +#include "xresources.h" /* Include .Xresources */ + +static char * cistrstr(const char *s, const char *sub); +static int (*fstrncmp)(const char *, const char *, size_t) = strncasecmp; +static char *(*fstrstr)(const char *, const char *) = cistrstr; + +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 +recalculatenumbers(void) +{ + unsigned int numer = 0, denom = 0; + struct item *item; + if (matchend) { + numer++; + for (item = matchend; item && item->left; item = item->left) + numer++; + } + for (item = items; item && item->text; item++) + denom++; + snprintf(numbers, NUMBERSBUFSIZE, "%d/%d", numer, denom); +} + +static void +calcoffsets(void) +{ + int i, n; + + if (lines > 0) + n = lines * columns * bh; + else + /* hide match count */ + if (hidematchcount) { + n = mw - (promptw + inputw + TEXTW(leftarrow) + TEXTW(rightarrow)); + } else { + n = mw - (promptw + inputw + TEXTW(leftarrow) + TEXTW(rightarrow) + TEXTW(numbers)); + } + /* 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 : MIN(TEXTWM(next->text), n)) > n) + break; + for (i = 0, prev = curr; prev && prev->left; prev = prev->left) + if ((i += (lines > 0) ? bh : MIN(TEXTWM(prev->left->text), n)) > n) + break; +} + +static int +max_textw(void) +{ + int len = 0; + for (struct item *item = items; item && item->text; item++) + len = MAX(TEXTW(item->text), len); + return len; +} + +static void +cleanup(void) +{ + size_t i; + + XUngrabKey(dpy, AnyKey, AnyModifier, root); + for (i = 0; i < SchemeLast; i++) + free(scheme[i]); + drw_free(drw); + XSync(dpy, False); + XCloseDisplay(dpy); +} + +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 void +drawhighlights(struct item *item, int x, int y, int maxw) +{ + char restorechar, tokens[sizeof text], *highlight, *token; + int indentx, highlightlen; + + drw_setscheme(drw, scheme[item == sel ? SchemeSelHighlight : SchemeNormHighlight]); + strcpy(tokens, text); + for (token = strtok(tokens, " "); token; token = strtok(NULL, " ")) { + highlight = fstrstr(item->text, token); + while (highlight) { + // Move item str end, calc width for highlight indent, & restore + highlightlen = highlight - item->text; + restorechar = *highlight; + item->text[highlightlen] = '\0'; + indentx = TEXTW(item->text); + item->text[highlightlen] = restorechar; + + // Move highlight str end, draw highlight, & restore + restorechar = highlight[strlen(token)]; + highlight[strlen(token)] = '\0'; + if (indentx - (lrpad / 2) - 1 < maxw) + drw_text( + drw, + x + indentx - (lrpad / 2) - 1, + y, + MIN(maxw - indentx, TEXTW(highlight) - lrpad), + bh, 0, highlight, 0, True + ); + highlight[strlen(token)] = restorechar; + + if (strlen(highlight) - strlen(token) < strlen(token)) break; + highlight = fstrstr(highlight + strlen(token), token); + } + } +} + +static int +drawitem(struct item *item, int x, int y, int w) +{ + if (item == sel) + drw_setscheme(drw, scheme[SchemeSel]); + else if (item->out) + drw_setscheme(drw, scheme[SchemeOut]); + else + drw_setscheme(drw, scheme[SchemeNorm]); + + char buffer[sizeof(item->text) + lrpad / 2]; + Clr scm[3]; + int wr, rd; + int pw; + int fg = 7; + int bg = 0; + int ignore = 0; + int bgfg = 0; + + if (item == sel) + memcpy(scm, scheme[SchemeSel], sizeof(scm)); + else if (item->out) + memcpy(scm, scheme[SchemeOut], sizeof(scm)); + else + memcpy(scm, scheme[SchemeNorm], sizeof(scm)); + + drw_setscheme(drw, scm); /* set scheme to what we copied */ + + for (wr = 0, rd = 0; item->text[rd]; rd++) { + if (item->text[rd] == '' && item->text[rd + 1] == '[') { + size_t alen = strspn(item->text + rd + 2, + "0123456789;"); + if (item->text[rd + alen + 2] == 'm') { + buffer[wr] = '\0'; + wr = 0; + + char *ep = item->text + rd + 1; + while (*ep != 'm') { + unsigned v = strtoul(ep + 1, &ep, 10); + if (ignore) + continue; + if (bgfg) { + if (bgfg < 4 && v == 5) { + bgfg <<= 1; + continue; + } + if (bgfg == 4) + scm[0] = textclrs[fg = v]; + else if (bgfg == 6) + scm[1] = textclrs[bg = v]; + ignore = 1; + + continue; + } + } + + rd += alen + 2; + continue; + } + } + buffer[wr++] = item->text[rd]; + } + buffer[wr] = '\0'; + + for (wr = 0, rd = 0; item->text[rd]; rd++) { + if (item->text[rd] == '' && item->text[rd + 1] == '[') { + size_t alen = strspn(item->text + rd + 2, + "0123456789;"); + if (item->text[rd + alen + 2] == 'm') { + buffer[wr] = '\0'; + pw = TEXTW(buffer) - lrpad / 2; + drw_text(drw, x, y, pw + lrpad / 2, bh, lrpad / 2, buffer, 0, False); + x += pw + lrpad / 2; + + char *ep = item->text + rd + 1; + while (*ep != 'm') { + unsigned v = strtoul(ep + 1, &ep, 10); + if (v == 1) { + fg |= 8; + scm[0] = textclrs[fg]; + } else if (v == 22) { + fg &= ~8; + scm[0] = textclrs[fg]; + } else if (v >= 30 && v <= 37) { + fg = v % 10 | (fg & 8); + scm[0] = textclrs[fg]; + } else if (v == 38) { + bgfg = 2; + } else if (v >= 40 && v <= 47) { + bg = v % 10; + scm[1] = textclrs[bg]; + } else if (v == 48) { + bgfg = 3; + } + } + + rd += alen + 2; + wr = 0; + + drw_setscheme(drw, scm); + continue; + } + } + buffer[wr++] = item->text[rd]; + } + + buffer[wr] = '\0'; + int r = drw_text(drw, x, y, w, bh, lrpad / 2, buffer, 0, False); + + drawhighlights(item, x, y, w); + return r; +} + +unsigned char +sixd_to_8bit(int x) +{ + return x == 0 ? 0 : 0x37 + 0x28 * x; +} + +static void +drawmenu(void) +{ + unsigned int curpos; + struct item *item; + int x = 0, y = 0, fh = drw->font->h, w; + char *censort; + + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, 0, 0, mw, mh, 1, 1); + + if (prompt && *prompt) { + if (colorprompt) { + drw_setscheme(drw, scheme[SchemePrompt]); + } + + x = drw_text(drw, x, 0, promptw, bh, lrpad / 2, prompt, 0, True); + } + /* draw input field */ + w = (lines > 0 || !matches) ? mw - x : inputw; + drw_setscheme(drw, scheme[SchemeNorm]); + if (passwd) { + censort = ecalloc(1, sizeof(text)); + memset(censort, '.', strlen(text)); + drw_text(drw, x, 0, w, bh, lrpad / 2, censort, 0, True); + free(censort); + } else drw_text(drw, x, 0, w, bh, lrpad / 2, text, 0, True); + + curpos = TEXTW(text) - TEXTW(&text[cursor]); + if ((curpos += lrpad / 2 - 1) < w) { + drw_setscheme(drw, scheme[SchemeCaret]); + drw_rect(drw, x + curpos, 2 + (bh - fh) / 2, 2, fh - 4, 1, 0); + } + + /* get match count */ + if (!hidematchcount) recalculatenumbers(); + + if (lines > 0) { + /* draw grid */ + int i = 0; + for (item = curr; item != next; item = item->right, i++) + drawitem( + item, + x + ((i / lines) * ((mw - x) / columns)), + y + (((i % lines) + 1) * bh), + (mw - x) / columns + ); + } else if (matches) { + /* draw horizontal list */ + x += inputw; + w = TEXTW(leftarrow); + if (curr->left) { + drw_setscheme(drw, scheme[SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, leftarrow, 0, True); + } + x += w; + for (item = curr; item != next; item = item->right) + if (hidematchcount) { + x = drawitem(item, x, 0, MIN(TEXTWM(item->text), mw - x - TEXTW(rightarrow))); + } else { + x = drawitem(item, x, 0, MIN(TEXTWM(item->text), mw - x - TEXTW(rightarrow) - TEXTW(numbers))); + } + if (next) { + w = TEXTW(rightarrow); + drw_setscheme(drw, scheme[SchemeNorm]); + + if (hidematchcount) { + drw_text(drw, mw - w, 0, w, bh, lrpad / 2, rightarrow, 0, True); + } else { + drw_text(drw, mw - w - TEXTW(numbers), 0, w, bh, lrpad / 2, rightarrow, 0, True); + } + } + } + + if (!hidematchcount) { + drw_setscheme(drw, scheme[SchemeNumber]); + drw_text(drw, mw - TEXTW(numbers), 0, TEXTW(numbers), bh, lrpad / 2, numbers, 0, False); + } + drw_map(drw, win, 0, 0, mw, mh); +} + +static void +grabfocus(void) +{ + struct timespec ts = { .tv_sec = 0, .tv_nsec = 10000000 }; + Window focuswin; + int i, revertwin; + + for (i = 0; i < 100; ++i) { + XGetInputFocus(dpy, &focuswin, &revertwin); + if (focuswin == win) + return; + if (managed) { + XTextProperty prop; + char *windowtitle = prompt != NULL ? prompt : "spmenu"; + Xutf8TextListToTextProperty(dpy, &windowtitle, 1, XUTF8StringStyle, &prop); + XSetWMName(dpy, win, &prop); + XSetTextProperty(dpy, win, &prop, XInternAtom(dpy, "_NET_WM_NAME", False)); + XFree(prop.value); + } else { + XSetInputFocus(dpy, win, RevertToParent, CurrentTime); + } + + nanosleep(&ts, NULL); + } + die("cannot grab focus"); +} + +static void +grabkeyboard(void) +{ + struct timespec ts = { .tv_sec = 0, .tv_nsec = 1000000 }; + int i; + + if (embed || managed) + return; + /* try to grab keyboard, we may have to wait for another process to ungrab */ + for (i = 0; i < 1000; i++) { + if (XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, + GrabModeAsync, CurrentTime) == GrabSuccess) + return; + nanosleep(&ts, NULL); + } + die("cannot grab keyboard"); +} + +int +compare_distance(const void *a, const void *b) +{ + struct item *da = *(struct item **) a; + struct item *db = *(struct item **) b; + + if (!db) + return 1; + if (!da) + return -1; + + return da->distance == db->distance ? 0 : da->distance < db->distance ? -1 : 1; +} + +void +fuzzymatch(void) +{ + /* bang - we have so much memory */ + struct item *it; + struct item **fuzzymatches = NULL; + char c; + int number_of_matches = 0, i, pidx, sidx, eidx; + int text_len = strlen(text), itext_len; + + matches = matchend = NULL; + + /* walk through all items */ + for (it = items; it && it->text; it++) { + if (text_len) { + itext_len = strlen(it->text); + pidx = 0; /* pointer */ + sidx = eidx = -1; /* start of match, end of match */ + /* walk through item text */ + for (i = 0; i < itext_len && (c = it->text[i]); i++) { + /* fuzzy match pattern */ + if (!fstrncmp(&text[pidx], &c, 1)) { + if(sidx == -1) + sidx = i; + pidx++; + if (pidx == text_len) { + eidx = i; + break; + } + } + } + /* build list of matches */ + if (eidx != -1) { + /* compute distance */ + /* add penalty if match starts late (log(sidx+2)) + * add penalty for long a match without many matching characters */ + it->distance = log(sidx + 2) + (double)(eidx - sidx - text_len); + /* fprintf(stderr, "distance %s %f\n", it->text, it->distance); */ + appenditem(it, &matches, &matchend); + number_of_matches++; + } + } else { + appenditem(it, &matches, &matchend); + } + } + + if (number_of_matches) { + /* initialize array with matches */ + if (!(fuzzymatches = realloc(fuzzymatches, number_of_matches * sizeof(struct item*)))) + die("cannot realloc %u bytes:", number_of_matches * sizeof(struct item*)); + for (i = 0, it = matches; it && i < number_of_matches; i++, it = it->right) { + fuzzymatches[i] = it; + } + /* sort matches according to distance */ + qsort(fuzzymatches, number_of_matches, sizeof(struct item*), compare_distance); + /* rebuild list of matches */ + matches = matchend = NULL; + for (i = 0, it = fuzzymatches[i]; i < number_of_matches && it && \ + it->text; i++, it = fuzzymatches[i]) { + appenditem(it, &matches, &matchend); + } + free(fuzzymatches); + } + + curr = sel = matches; + + for (i = 0; i < preselected; i++) { + if (sel && sel->right && (sel = sel->right) == next) { + curr = next; + calcoffsets(); + } + } + + calcoffsets(); +} + +static void +match(void) +{ + if (fuzzy) { + fuzzymatch(); + return; + } + 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 %u 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 +loadhistory(void) +{ + FILE *fp = NULL; + static size_t cap = 0; + size_t llen; + char *line; + + if (!histfile) { + return; + } + + fp = fopen(histfile, "r"); + if (!fp) { + return; + } + + for (;;) { + line = NULL; + llen = 0; + if (-1 == getline(&line, &llen, fp)) { + if (ferror(fp)) { + die("failed to read history"); + } + free(line); + break; + } + + if (cap == histsz) { + cap += 64 * sizeof(char*); + history = realloc(history, cap); + if (!history) { + die("failed to realloc memory"); + } + } + strtok(line, "\n"); + history[histsz] = line; + histsz++; + } + histpos = histsz; + + if (fclose(fp)) { + die("failed to close file %s", histfile); + } +} + +static void +navhistory(int dir) +{ + static char def[BUFSIZ]; + char *p = NULL; + size_t len = 0; + + if (!history || histpos + 1 == 0) + return; + + if (histsz == histpos) { + strncpy(def, text, sizeof(def)); + } + + switch(dir) { + case 1: + if (histpos < histsz - 1) { + p = history[++histpos]; + } else if (histpos == histsz - 1) { + p = def; + histpos++; + } + break; + case -1: + if (histpos > 0) { + p = history[--histpos]; + } + break; + } + if (p == NULL) { + return; + } + + len = MIN(strlen(p), BUFSIZ - 1); + strncpy(text, p, len); + text[len] = '\0'; + cursor = len; + match(); +} + +static void +savehistory(char *input) +{ + unsigned int i; + FILE *fp; + + if (!histfile || + 0 == maxhist || + 0 == strlen(input)) { + goto out; + } + + fp = fopen(histfile, "w"); + if (!fp) { + die("failed to open %s", histfile); + } + for (i = histsz < maxhist ? 0 : histsz - maxhist; i < histsz; i++) { + if (0 >= fprintf(fp, "%s\n", history[i])) { + die("failed to write to %s", histfile); + } + } + if (histsz == 0 || !histnodup || (histsz > 0 && strcmp(input, history[histsz-1]) != 0)) { /* TODO */ + if (0 >= fputs(input, fp)) { + die("failed to write to %s", histfile); + } + } + if (fclose(fp)) { + die("failed to close file %s", histfile); + } + +out: + for (i = 0; i < histsz; i++) { + free(history[i]); + } + free(history); +} + +static void +keypress(XKeyEvent *ev) +{ + char buf[64]; + int len; + KeySym ksym = NoSymbol; + Status status; + int i, offscreen = 0; + struct item *tmpsel; + + len = XmbLookupString(xic, ev, buf, sizeof buf, &ksym, &status); + switch (status) { + default: /* XLookupNone, XBufferOverflow */ + return; + case XLookupChars: /* composed string from input method */ + goto insert; + case XLookupKeySym: + case XLookupBoth: /* a KeySym and a string are returned: use keysym */ + break; + } + + if (ev->state & ControlMask) { + switch(ksym) { + case XK_a: ksym = XK_Home; break; + case XK_b: ksym = XK_Left; break; + case XK_c: ksym = XK_Escape; break; + case XK_d: ksym = XK_Delete; break; + case XK_e: ksym = XK_End; break; + case XK_f: ksym = XK_Right; break; + case XK_g: ksym = XK_Escape; break; + case XK_h: ksym = XK_BackSpace; break; + case XK_i: ksym = XK_Tab; break; + case XK_j: /* fallthrough */ + case XK_J: /* fallthrough */ + case XK_m: /* fallthrough */ + case XK_M: ksym = XK_Return; ev->state &= ~ControlMask; break; + case XK_n: ksym = XK_Down; break; + case XK_p: ksym = XK_Up; break; + + case XK_k: /* delete right */ + text[cursor] = '\0'; + match(); + break; + case XK_u: /* delete left */ + insert(NULL, 0 - cursor); + break; + case XK_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); + break; + + /* v to paste */ + case XK_v: + case XK_V: + XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY, + utf8, utf8, win, CurrentTime); + return; + + case XK_y: /* paste selection */ + case XK_Y: + XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY, + utf8, utf8, win, CurrentTime); + return; + case XK_r: + if (histfile) { + if (!backup_items) { + backup_items = items; + items = calloc(histsz + 1, sizeof(struct item)); + if (!items) { + die("cannot allocate memory"); + } + + for (i = 0; i < histsz; i++) { + items[i].text = history[i]; + } + } else { + free(items); + items = backup_items; + backup_items = NULL; + } + } + match(); + goto draw; + case XK_Left: + case XK_KP_Left: + movewordedge(-1); + goto draw; + case XK_Right: + case XK_KP_Right: + movewordedge(+1); + goto draw; + case XK_Return: + case XK_KP_Enter: + break; + case XK_bracketleft: + cleanup(); + exit(1); + default: + return; + } + } else if (ev->state & Mod1Mask) { + switch(ksym) { + case XK_b: + movewordedge(-1); + goto draw; + case XK_f: + movewordedge(+1); + goto draw; + case XK_g: ksym = XK_Home; break; + case XK_G: ksym = XK_End; break; + case XK_h: ksym = XK_Up; break; + case XK_j: ksym = XK_Next; break; + case XK_k: ksym = XK_Prior; break; + case XK_l: ksym = XK_Down; break; + case XK_p: + navhistory(-1); + buf[0]=0; + break; + case XK_n: + navhistory(1); + buf[0]=0; + break; + default: + return; + } + } + + switch(ksym) { + default: + insert: + if (!iscntrl(*buf) && type) + insert(buf, len); + break; + case XK_Delete: + case XK_KP_Delete: + if (text[cursor] == '\0') + return; + cursor = nextrune(+1); + /* fallthrough */ + case XK_BackSpace: + if (cursor == 0) + return; + insert(NULL, nextrune(-1) - cursor); + break; + case XK_End: + case XK_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 XK_Escape: + cleanup(); + exit(1); + case XK_Home: + case XK_KP_Home: + if (sel == matches) { + cursor = 0; + break; + } + sel = curr = matches; + calcoffsets(); + break; + case XK_Left: + if (columns > 1) { + if (!sel) + return; + tmpsel = sel; + for (i = 0; i < lines; i++) { + if (!tmpsel->left || tmpsel->left->right != tmpsel) { + if (offscreen) + break; + return; + } + if (tmpsel == curr) + offscreen = 1; + tmpsel = tmpsel->left; + } + sel = tmpsel; + if (offscreen) { + curr = prev; + calcoffsets(); + } + break; + } + case XK_KP_Left: + if (cursor > 0 && (!sel || !sel->left || lines > 0)) { + cursor = nextrune(-1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XK_Up: + case XK_KP_Up: + if (sel && sel->left && (sel = sel->left)->right == curr) { + curr = prev; + calcoffsets(); + } + break; + case XK_Next: + case XK_KP_Next: + if (!next) + return; + sel = curr = next; + calcoffsets(); + break; + case XK_Prior: + case XK_KP_Prior: + if (!prev) + return; + sel = curr = prev; + calcoffsets(); + break; + case XK_Return: + case XK_KP_Enter: + puts((sel && !(ev->state & ShiftMask)) ? sel->text : text); + if (!(ev->state & ControlMask)) { + savehistory((sel && !(ev->state & ShiftMask)) + ? sel->text : text); + cleanup(); + exit(0); + } + if (sel) + sel->out = 1; + break; + case XK_Right: + if (columns > 1) { + if (!sel) + return; + tmpsel = sel; + for (i = 0; i < lines; i++) { + if (!tmpsel->right || tmpsel->right->left != tmpsel) { + if (offscreen) + break; + return; + } + tmpsel = tmpsel->right; + if (tmpsel == next) + offscreen = 1; + } + sel = tmpsel; + if (offscreen) { + curr = next; + calcoffsets(); + } + break; + } + case XK_KP_Right: + if (text[cursor] != '\0') { + cursor = nextrune(+1); + break; + } + if (lines > 0) + return; + /* fallthrough */ + case XK_Down: + case XK_KP_Down: + if (sel && sel->right && (sel = sel->right) == next) { + curr = next; + calcoffsets(); + } + break; + case XK_Tab: + if (!sel) + return; + strncpy(text, sel->text, sizeof text - 1); + text[sizeof text - 1] = '\0'; + cursor = strlen(text); + match(); + break; + } + +draw: + drawmenu(); +} + +static void +mousemove(XEvent *e) +{ + struct item *item; + XPointerMovedEvent *ev = &e->xmotion; + int x = 0, y = 0, h = bh, w, item_num = 0; + + if (lines > 0) { + w = mw - x; + for (item = curr; item != next; item = item->right) { + if (item_num++ == lines) { + item_num = 1; + x += w / columns; + y = 0; + } + y += h; + if (ev->y >= y && ev->y <= (y + h) && ev->x >= x && ev->x <= (x + w / columns)) { + sel = item; + calcoffsets(); + drawmenu(); + return; + } + } + } else if (matches) { + x += inputw + promptw; + w = TEXTW(leftarrow); + for (item = curr; item != next; item = item->right) { + x += w; + w = MIN(TEXTW(item->text), mw - x - TEXTW(rightarrow)); + if (ev->x >= x && ev->x <= x + w) { + sel = item; + calcoffsets(); + drawmenu(); + return; + } + } + } +} + +static void +buttonpress(XEvent *e) +{ + struct item *item; + XButtonPressedEvent *ev = &e->xbutton; + int x = 0, y = 0, h = bh, w, item_num = 0; + + if (ev->window != win) + return; + + /* right-click: exit */ + if (ev->button == Button3) + exit(1); + + if (prompt && *prompt) + x += promptw; + + /* input field */ + w = (lines > 0 || !matches) ? mw - x : inputw; + + /* left-click on input: clear input, + * NOTE: if there is no left-arrow the space for < is reserved so + * add that to the input width */ + if (ev->button == Button1 && + ((lines <= 0 && ev->x >= 0 && ev->x <= x + w + + ((!prev || !curr->left) ? TEXTW(leftarrow) : 0)) || + (lines > 0 && ev->y >= y && ev->y <= y + h))) { + insert(NULL, -cursor); + drawmenu(); + return; + } + /* middle-mouse click: paste selection */ + if (ev->button == Button2) { + XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY, + utf8, utf8, win, CurrentTime); + drawmenu(); + return; + } + /* scroll up */ + if (ev->button == Button4 && prev) { + sel = curr = prev; + calcoffsets(); + drawmenu(); + return; + } + /* scroll down */ + if (ev->button == Button5 && next) { + sel = curr = next; + calcoffsets(); + drawmenu(); + return; + } + if (ev->button != Button1) + return; + if (ev->state & ~ControlMask) + return; + if (lines > 0) { + /* vertical list: (ctrl)left-click on item */ + w = mw - x; + for (item = curr; item != next; item = item->right) { + if (item_num++ == lines){ + item_num = 1; + x += w / columns; + y = 0; + } + y += h; + if (ev->y >= y && ev->y <= (y + h) && + ev->x >= x && ev->x <= (x + w / columns)) { + puts(item->text); + if (!(ev->state & ControlMask)) + exit(0); + sel = item; + if (sel) { + sel->out = 1; + drawmenu(); + } + return; + } + } + } else if (matches) { + /* left-click on left arrow */ + x += inputw; + w = TEXTW(leftarrow); + if (prev && curr->left) { + if (ev->x >= x && ev->x <= x + w) { + sel = curr = prev; + calcoffsets(); + drawmenu(); + return; + } + } + /* horizontal list: (ctrl)left-click on item */ + for (item = curr; item != next; item = item->right) { + x += w; + w = MIN(TEXTW(item->text), mw - x - TEXTW(rightarrow)); + if (ev->x >= x && ev->x <= x + w) { + puts(item->text); + if (!(ev->state & ControlMask)) + exit(0); + sel = item; + if (sel) { + sel->out = 1; + drawmenu(); + } + return; + } + } + /* left-click on right arrow */ + w = TEXTW(rightarrow); + x = mw - w; + if (next && ev->x >= x && ev->x <= x + w) { + sel = curr = next; + calcoffsets(); + drawmenu(); + return; + } + } +} + +static void +paste(void) +{ + char *p, *q; + int di; + unsigned long dl; + Atom da; + + /* we have been given the current selection, now insert it into input */ + if (XGetWindowProperty(dpy, win, utf8, 0, (sizeof text / 4) + 1, False, + utf8, &da, &di, &dl, &dl, (unsigned char **)&p) + == Success && p) { + insert(p, (q = strchr(p, '\n')) ? q - p : (ssize_t)strlen(p)); + XFree(p); + } + drawmenu(); +} + +static void +xinitvisual() +{ + XVisualInfo *infos; + XRenderPictFormat *fmt; + int nitems; + int i; + + XVisualInfo tpl = { + .screen = screen, + .depth = 32, + .class = TrueColor + }; + long masks = VisualScreenMask | VisualDepthMask | VisualClassMask; + + infos = XGetVisualInfo(dpy, masks, &tpl, &nitems); + visual = NULL; + for(i = 0; i < nitems; i ++) { + fmt = XRenderFindVisualFormat(dpy, infos[i].visual); + if (fmt->type == PictTypeDirect && fmt->direct.alphaMask) { + visual = infos[i].visual; + depth = infos[i].depth; + cmap = XCreateColormap(dpy, root, visual, AllocNone); + useargb = 1; + break; + } + } + + XFree(infos); + + if (!visual || !alpha) { + visual = DefaultVisual(dpy, screen); + depth = DefaultDepth(dpy, screen); + cmap = DefaultColormap(dpy, screen); + } +} + +static void +readstdin(void) +{ + char buf[sizeof text], *p; + size_t i, imax = 0, itemsiz = 0; + unsigned int tmpmax = 0; + if(passwd){ + inputw = lines = 0; + return; + } + + + /* read each line from stdin and add it to the item list */ + for (i = 0; fgets(buf, sizeof buf, stdin); i++) { + if (i + 1 >= itemsiz) { + itemsiz += 256; + if (!(items = realloc(items, itemsiz * sizeof(*items)))) + die("cannot realloc %zu bytes:", itemsiz * sizeof(*items)); + } + if ((p = strchr(buf, '\n'))) + *p = '\0'; + if (!(items[i].text = strdup(buf))) + die("cannot strdup %u bytes:", strlen(buf) + 1); + items[i].out = 0; + drw_font_getexts(drw->font, buf, strlen(buf), &tmpmax, NULL, True); + if (tmpmax > inputw) { + inputw = tmpmax; + imax = i; + } + } + if (items) + items[i].text = NULL; + inputw = items ? TEXTWM(items[imax].text) : 0; + lines = MIN(lines, i); +} + +void +resource_load(XrmDatabase db, char *name, enum resource_type rtype, void *dst) +{ + char *sdst = NULL; + int *idst = NULL; + float *fdst = NULL; + sdst = dst; + idst = dst; + fdst = dst; + char fullname[256]; + char *type; + XrmValue ret; + snprintf(fullname, sizeof(fullname), "%s.%s", "spmenu", name); + fullname[sizeof(fullname) - 1] = '\0'; + XrmGetResource(db, fullname, "*", &type, &ret); + if (!(ret.addr == NULL || strncmp("String", type, 64))) + { + switch (rtype) { + case STRING: + strcpy(sdst, ret.addr); + break; + case INTEGER: + *idst = strtoul(ret.addr, NULL, 10); + break; + case FLOAT: + *fdst = strtof(ret.addr, NULL); + break; + } + } +} + +void +load_xresources(void) +{ + Display *display; + char *resm; + XrmDatabase db; + ResourcePref *p; + display = XOpenDisplay(NULL); + resm = XResourceManagerString(display); + if (!resm) + return; + db = XrmGetStringDatabase(resm); + for (p = resources; p < resources + LENGTH(resources); p++) + resource_load(db, p->name, p->type, p->dst); + XCloseDisplay(display); +} + +static void +run(void) +{ + XEvent ev; + + while (!XNextEvent(dpy, &ev)) { + if (XFilterEvent(&ev, win)) + continue; + switch(ev.type) { + case DestroyNotify: + if (ev.xdestroywindow.window != win) + break; + cleanup(); + exit(1); + case ButtonPress: + buttonpress(&ev); + break; + case MotionNotify: + mousemove(&ev); + break; + case Expose: + if (ev.xexpose.count == 0) + drw_map(drw, win, 0, 0, mw, mh); + break; + case FocusIn: + /* regrab focus from parent window */ + if (ev.xfocus.window != win) + grabfocus(); + break; + case KeyPress: + keypress(&ev.xkey); + break; + case SelectionNotify: + if (ev.xselection.property == utf8) + paste(); + break; + case VisibilityNotify: + if (ev.xvisibility.state != VisibilityUnobscured) + XRaiseWindow(dpy, win); + break; + } + } +} + +static void +setup(void) +{ + int x, y, i, j; + unsigned int du; + XSetWindowAttributes swa; + XIM xim; + Window w, dw, *dws; + XWindowAttributes wa; + XClassHint ch = { class, class }; +#ifdef XINERAMA + XineramaScreenInfo *info; + Window pw; + int a, di, n, area = 0; +#endif + char cbuf[8]; + /* init appearance */ + for (j = 0; j < SchemeLast; j++) { + scheme[j] = drw_scm_create(drw, colors[j], alphas[j], 2); + } + + for (i = 0; i < LENGTH(textcolors) && i < LENGTH(textclrs); i++) + drw_clr_create(drw, &textclrs[i], textcolors[i], 0); + if (i == 0 && colorsupport) + drw_clr_create(drw, &textclrs[i++], "#000000", 0); + for (; i < 7; i++) { + if (!colorsupport) + break; + snprintf(cbuf, sizeof(cbuf), "#%02x%02x%02x", + !!(i & 1) * 0x7f, + !!(i & 2) * 0x7f, + !!(i & 4) * 0x7f); + drw_clr_create(drw, &textclrs[i], cbuf, 0); + } + if (i == 7 && colorsupport) + drw_clr_create(drw, &textclrs[i++], "#000000", 0); + if (i == 8 && colorsupport) + drw_clr_create(drw, &textclrs[i++], "#333333", 0); + for (; i < 16; i++) { + if (!colorsupport) + break; + snprintf(cbuf, sizeof(cbuf), "#%02x%02x%02x", + !!(i & 1) * 0xff, + !!(i & 2) * 0xff, + !!(i & 4) * 0xff); + drw_clr_create(drw, &textclrs[i], cbuf, 0); + } + for (; i < 6 * 6 * 6 + 16; i++) { + if (!colorsupport) + break; + snprintf(cbuf, sizeof(cbuf), "#%02x%02x%02x", + sixd_to_8bit(((i - 16) / 36) % 6), + sixd_to_8bit(((i - 16) / 6) % 6), + sixd_to_8bit(((i - 16)) % 6)); + drw_clr_create(drw, &textclrs[i], cbuf, 0); + } + for (; i < 256; i++) { + if (!colorsupport) + break; + snprintf(cbuf, sizeof(cbuf), "#%02x%02x%02x", + 0x08 + (i - 6 * 6 * 6 - 16) * 0x0a, + 0x08 + (i - 6 * 6 * 6 - 16) * 0x0a, + 0x08 + (i - 6 * 6 * 6 - 16) * 0x0a); + drw_clr_create(drw, &textclrs[i], cbuf, 0); + } + + clip = XInternAtom(dpy, "CLIPBOARD", False); + utf8 = XInternAtom(dpy, "UTF8_STRING", False); + + /* calculate menu geometry */ + bh = drw->font->h + 2; + bh = MAX(bh, lineheight); /* make a menu line AT LEAST 'lineheight' tall */ + lines = MAX(lines, 0); + mh = (lines + 1) * bh; + promptw = (prompt && *prompt) ? TEXTWM(prompt) - lrpad / 4 : 0; +#ifdef XINERAMA + i = 0; + if (parentwin == root && (info = XineramaQueryScreens(dpy, &n))) { + XGetInputFocus(dpy, &w, &di); + if (mon >= 0 && mon < n) + i = mon; + else if (w != root && w != PointerRoot && w != None) { + /* find top-level window containing current input focus */ + do { + if (XQueryTree(dpy, (pw = w), &dw, &w, &dws, &du) && dws) + XFree(dws); + } while (w != root && w != pw); + /* find xinerama screen with which the window intersects most */ + if (XGetWindowAttributes(dpy, pw, &wa)) + for (j = 0; j < n; j++) + if ((a = INTERSECT(wa.x, wa.y, wa.width, wa.height, info[j])) > area) { + area = a; + i = j; + } + } + /* no focused window is on screen, so use pointer location instead */ + if (mon < 0 && !area && XQueryPointer(dpy, root, &dw, &dw, &x, &y, &di, &di, &du)) + for (i = 0; i < n; i++) + if (INTERSECT(x, y, 1, 1, info[i])) + break; + + if (centered) { + mw = MIN(MAX(max_textw() + promptw, minwidth), info[i].width); + x = info[i].x_org + ((info[i].width - mw) / 2); + //y = info[i].y_org + 0; + y = info[i].y_org + ((info[i].height - mh) / 2); + } else { + x = info[i].x_org + dmx; + y = info[i].y_org + (menuposition ? 0 : info[i].height - mh - dmy); + //y = info[i].y_org + 0; + mw = (dmw>0 ? dmw : info[i].width); + } + + XFree(info); + } else +#endif + { + if (!XGetWindowAttributes(dpy, parentwin, &wa)) + die("could not get embedding window attributes: 0x%lx", + parentwin); + + if (centered) { + mw = MIN(MAX(max_textw() + promptw, minwidth), wa.width); + x = (wa.width - mw) / 2; + y = (wa.height - mh) / 2; + } else { + x = 0; + y = 0; + mw = wa.width; + } + } + inputw = MIN(inputw, mw/3); + match(); + + /* create menu window */ + swa.override_redirect = managed ? False : True; + swa.background_pixel = 0; + swa.colormap = cmap; + swa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask | ButtonPressMask | PointerMotionMask; + + if (!bordercentered) { + win = XCreateWindow(dpy, parentwin, x + sp, y + vp, mw - 2 * sp, mh, borderwidth, + depth, InputOutput, visual, + CWOverrideRedirect|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask, &swa); + } else { + if (!centered) { + win = XCreateWindow(dpy, parentwin, x + sp, y + vp, mw - 2 * sp, mh, 0, + depth, InputOutput, visual, + CWOverrideRedirect|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask, &swa); + } else { + win = XCreateWindow(dpy, parentwin, x + sp, y + vp, mw - 2 * sp, mh, borderwidth, + depth, InputOutput, visual, + CWOverrideRedirect|CWBackPixel|CWBorderPixel|CWColormap|CWEventMask, &swa); + } + + } + XSetWindowBorder(dpy, win, scheme[SchemeSel][ColBg].pixel); + XSetClassHint(dpy, win, &ch); + + + /* input methods */ + if ((xim = XOpenIM(dpy, NULL, NULL, NULL)) == NULL) + die("XOpenIM failed: could not open input device"); + + xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, + XNClientWindow, win, XNFocusWindow, win, NULL); + + XMapRaised(dpy, win); + if (embed) { + XSelectInput(dpy, parentwin, FocusChangeMask | SubstructureNotifyMask); + if (XQueryTree(dpy, parentwin, &dw, &w, &dws, &du) && dws) { + for (i = 0; i < du && dws[i] != win; ++i) + XSelectInput(dpy, dws[i], FocusChangeMask); + XFree(dws); + } + grabfocus(); + } + drw_resize(drw, mw, mh); + drawmenu(); +} + +static void +usage(void) +{ + fputs("spmenu: dynamic menu\n\n" + "- Arguments -\n" + "spmenu -l Set line count to stdin\n" + "spmenu -h Set spmenu height to \n" + "spmenu -g Set the number of grids to \n" + "spmenu -f Grabs keyboard before reading stdin\n" + "spmenu -F Enable fuzzy matching\n" + "spmenu -P Hide characters\n" + "spmenu -p Set spmenu prompt text to \n" + "spmenu -a Enable alpha\n" + "spmenu -na Disable alpha\n" + "spmenu -cp Color prompt\n" + "spmenu -ncp Don't color prompt\n" + "spmenu -t Allow the user to type\n" + "spmenu -nt Don't allow typing, the user must select an option\n" + "spmenu -x Offset spmenu x position by \n" + "spmenu -y Offset spmenu y position by \n" + "spmenu -n Preselect in the list of items\n" + "spmenu -z Width of the spmenu window\n" + "spmenu -bc Display border around prompt when centered\n" + "spmenu -bw Width of the border. 0 will disable the border\n" + "spmenu -s Use case-sensitive matching\n" + "spmenu -i Use case-insnsitive matching\n" + "spmenu -s Use case-snsitive matching\n" + "spmenu -to Position spmenu at the top of the screen\n" + "spmenu -b Position spmenu at the bottom of the screen\n" + "spmenu -c Position spmenu at the center of the screen\n" + "spmenu -hmc Hide match count\n" + "spmenu -smc Show match count\n" + "spmenu -xrdb Load .Xresources on runtime\n" + "spmenu -nxrdb Don't load .Xresources on runtime\n" + "spmenu -m Specify a monitor to run spmenu on\n" + "spmenu -w Embed spmenu inside \n" + "spmenu -H Specify a path to save the history to\n" + "spmenu -lp Set the vertical padding\n" + "spmenu -hp Set the horizontal padding\n" + "spmenu -la Set the left arrow to \n" + "spmenu -ra Set the right arrow to \n" + "spmenu -wm Spawn spmenu as a window manager controlled client/window. Useful for testing\n" + "spmenu -v Print spmenu version to stdout\n" + "\n" + "- Color arguments -\n" + "spmenu -fn Set the spmenu font to \n" + "spmenu -nb Set the normal background color\n" + "spmenu -nf Set the normal foreground color\n" + "spmenu -sb Set the selected background color\n" + "spmenu -sf Set the selected foreground color\n" + "spmenu -cc Set the caret color\n" + "spmenu -nhf Set the normal highlight foreground color\n" + "spmenu -nhb Set the normal highlight background color\n" + "spmenu -shf Set the selected highlight foreground color\n" + "spmenu -shb Set the selected highlight background color\n" + "spmenu -shb Set the selected highlight background color\n" + "spmenu -nfg Set the foreground color for the match count\n" + "spmenu -nbg Set the background color for the match count\n" + "spmenu -sgr0 Set the SGR 0 color\n" + "spmenu -sgr1 Set the SGR 1 color\n" + "spmenu -sgr2 Set the SGR 2 color\n" + "spmenu -sgr3 Set the SGR 3 color\n" + "spmenu -sgr4 Set the SGR 4 color\n" + "spmenu -sgr5 Set the SGR 5 color\n" + "spmenu -sgr6 Set the SGR 6 color\n" + "spmenu -sgr7 Set the SGR 7 color\n" + "spmenu -sgr8 Set the SGR 8 color\n" + "spmenu -sgr9 Set the SGR 9 color\n" + "spmenu -sgr10 Set the SGR 10 color\n" + "spmenu -sgr11 Set the SGR 11 color\n" + "spmenu -sgr12 Set the SGR 12 color\n" + "spmenu -sgr13 Set the SGR 13 color\n" + "spmenu -sgr14 Set the SGR 14 color\n" + "spmenu -sgr15 Set the SGR 15 color\n" + "\n" + "- Example usage -\n" + "`echo 'Hello\\nWorld' | spmenu -l 2` will allow you to pick either 'Hello' or 'World'. The selected then gets printed to stdout. This means you can pipe it into `sed` or `grep`.\n" + , stdout); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + XWindowAttributes wa; + int i, fast = 0; + + XrmInitialize(); + + for (i = 1; i < argc; i++) { + /* xrdb first as it overrides other options */ + if (!strcmp(argv[i], "-xrdb")) /* xresources */ + xresources = 1; + else if (!strcmp(argv[i], "-nxrdb")) /* no xresources */ + xresources = 0; + } + + if (xresources) { + load_xresources(); + } + + if (casesensitive) { + fstrncmp = strncmp; + fstrstr = strstr; + } + + /* these options take no arguments */ + for (i = 1; i < argc; i++) + if (!strcmp(argv[i], "-v")) { /* prints version information */ + puts("spmenu-"VERSION); + exit(0); + } else if (!strcmp(argv[i], "-b")) { /* appears at the bottom of the screen */ + menuposition = 0; + } else if (!strcmp(argv[i], "-to")) { /* appears at the top of the screen */ + menuposition = 1; + } else if (!strcmp(argv[i], "-c")) { /* appears at the center of the screen */ + centered = 1; + } else if (!strcmp(argv[i], "-bc")) { /* draw border when centered */ + bordercentered = 1; + centered = 1; + } else if (!strcmp(argv[i], "-f")) /* grabs keyboard before reading stdin */ + fast = 1; + else if (!strcmp(argv[i], "-xrdb")) /* xresources */ + xresources = 1; + else if (!strcmp(argv[i], "-nxrdb")) /* no xresources */ + xresources = 0; + else if (!strcmp(argv[i], "-F")) /* fuzzy matching */ + fuzzy = 0; + else if (!strcmp(argv[i], "-s")) { /* case-sensitive item matching */ + fstrncmp = strncmp; + fstrstr = strstr; + } else if (!strcmp(argv[i], "-i")) { /* case-sensitive item matching, for compatibility reasons */ + fstrncmp = strncasecmp; + fstrstr = cistrstr; + } else if (!strcmp(argv[i], "-wm")) { /* display as managed wm window */ + managed = 1; + } else if (!strcmp(argv[i], "-na")) { /* disable alpha */ + alpha = 0; + } else if (!strcmp(argv[i], "-a")) { /* alpha */ + alpha = 1; + } else if (!strcmp(argv[i], "-cp")) { /* color prompt */ + colorprompt = 1; + } else if (!strcmp(argv[i], "-ncp")) { /* no color prompt */ + colorprompt = 0; + } else if (!strcmp(argv[i], "-t")) { /* allow the user to type */ + type = 1; + } else if (!strcmp(argv[i], "-nt")) { /* don't allow the user to type */ + type = 0; + } else if (!strcmp(argv[i], "-P")) { /* is the input a password */ + passwd = 1; + } else if (!strcmp(argv[i], "-hmc")) { /* hide match count */ + hidematchcount = 1; + } else if (!strcmp(argv[i], "-smc")) { /* don't hide match count */ + hidematchcount = 0; + } else if (i + 1 == argc) + usage(); + + /* these options take one argument */ + else if (!strcmp(argv[i], "-g")) { /* number of columns in grid */ + columns = atoi(argv[++i]); + if (lines == 0) lines = 1; + } else if (!strcmp(argv[i], "-l")) { /* number of lines in grid */ + lines = atoi(argv[++i]); + } else if (!strcmp(argv[i], "-h")) { /* minimum height of one menu line */ + lineheight = atoi(argv[++i]); + lineheight = MAX(lineheight, minlineheight); + if (columns == 0) columns = 1; + } else if (!strcmp(argv[i], "-lp")) { + menupaddingv = atoi(argv[++i]); + } else if (!strcmp(argv[i], "-hp")) { + menupaddingh = atoi(argv[++i]); + } else if (!strcmp(argv[i], "-la")) { + leftarrow = argv[++i]; + } else if (!strcmp(argv[i], "-ra")) { + rightarrow = argv[++i]; + } else if (!strcmp(argv[i], "-m")) /* monitor */ + mon = atoi(argv[++i]); + else if (!strcmp(argv[i], "-bw")) /* border width */ + borderwidth = atoi(argv[++i]); + else if (!strcmp(argv[i], "-H")) /* hist file location */ + histfile = argv[++i]; + else if (!strcmp(argv[i], "-x")) /* window x offset */ + dmx = atoi(argv[++i]); + else if (!strcmp(argv[i], "-y")) /* window y offset (from bottom up if -b) */ + dmy = atoi(argv[++i]); + else if (!strcmp(argv[i], "-z")) /* make spmenu this wide */ + dmw = atoi(argv[++i]); + else if (!strcmp(argv[i], "-p")) /* adds prompt to left of input field */ + prompt = argv[++i]; + else if (!strcmp(argv[i], "-fn")) /* font or font set */ + strcpy(font, argv[++i]); /* font[0] = argv[++i]; */ + else if (!strcmp(argv[i], "-nb")) /* normal background color */ + colors[SchemeNorm][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-nf")) /* normal foreground color */ + colors[SchemeNorm][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-sb")) /* selected background color */ + colors[SchemeSel][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-sf")) /* selected foreground color */ + colors[SchemeSel][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-shf")) /* selected highlight foreground color */ + colors[SchemeSelHighlight][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-nhf")) /* normal highlight foreground color */ + colors[SchemeNormHighlight][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-shb")) /* selected highlight foreground color */ + colors[SchemeSelHighlight][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-nhb")) /* normal highlight foreground color */ + colors[SchemeNormHighlight][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-nbg")) /* numbgcolor */ + colors[SchemeNumber][ColBg] = argv[++i]; + else if (!strcmp(argv[i], "-nfg")) /* numfgcolor */ + colors[SchemeNumber][ColFg] = argv[++i]; + /* sgr colors */ + else if (!strcmp(argv[i], "-sgr0")) textcolors[0] = argv[++i]; /* sgr color 0 */ + else if (!strcmp(argv[i], "-sgr1")) textcolors[1] = argv[++i]; /* sgr color 1 */ + else if (!strcmp(argv[i], "-sgr2")) textcolors[2] = argv[++i]; /* sgr color 2 */ + else if (!strcmp(argv[i], "-sgr3")) textcolors[3] = argv[++i]; /* sgr color 3 */ + else if (!strcmp(argv[i], "-sgr4")) textcolors[4] = argv[++i]; /* sgr color 4 */ + else if (!strcmp(argv[i], "-sgr5")) textcolors[5] = argv[++i]; /* sgr color 5 */ + else if (!strcmp(argv[i], "-sgr6")) textcolors[6] = argv[++i]; /* sgr color 6 */ + else if (!strcmp(argv[i], "-sgr7")) textcolors[7] = argv[++i]; /* sgr color 7 */ + else if (!strcmp(argv[i], "-sgr8")) textcolors[8] = argv[++i]; /* sgr color 8 */ + else if (!strcmp(argv[i], "-sgr9")) textcolors[9] = argv[++i]; /* sgr color 9 */ + else if (!strcmp(argv[i], "-sgr10")) textcolors[10] = argv[++i]; /* sgr color 10 */ + else if (!strcmp(argv[i], "-sgr11")) textcolors[11] = argv[++i]; /* sgr color 11 */ + else if (!strcmp(argv[i], "-sgr12")) textcolors[12] = argv[++i]; /* sgr color 12 */ + else if (!strcmp(argv[i], "-sgr13")) textcolors[13] = argv[++i]; /* sgr color 13 */ + else if (!strcmp(argv[i], "-sgr14")) textcolors[14] = argv[++i]; /* sgr color 14 */ + else if (!strcmp(argv[i], "-sgr15")) textcolors[15] = argv[++i]; /* sgr color 15 */ + else if (!strcmp(argv[i], "-cc")) /* caret color */ + colors[SchemeCaret][ColFg] = argv[++i]; + else if (!strcmp(argv[i], "-w")) /* embedding window id */ + embed = argv[++i]; + else if (!strcmp(argv[i], "-n")) /* preselected item */ + preselected = atoi(argv[++i]); + else + usage(); + + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fputs("warning: no locale support\n", stderr); + if (!(dpy = XOpenDisplay(NULL))) + die("cannot open display"); + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + if (!embed || !(parentwin = strtol(embed, NULL, 0))) + parentwin = root; + if (!XGetWindowAttributes(dpy, parentwin, &wa)) + die("could not get embedding window attributes: 0x%lx", + parentwin); + xinitvisual(); + drw = drw_create(dpy, screen, root, wa.width, wa.height, visual, depth, cmap); + + if (!drw_font_create(drw, font)) + die("no fonts could be loaded."); + lrpad = drw->font->h; + + sp = menupaddingh; + vp = (menuposition == 1) ? menupaddingv : - menupaddingv; + +#ifdef __OpenBSD__ + if (pledge("stdio rpath", NULL) == -1) + die("pledge"); +#endif + + loadhistory(); + + if (fast && !isatty(0)) { + grabkeyboard(); + readstdin(); + } else { + readstdin(); + grabkeyboard(); + } + setup(); + run(); + + return 1; /* unreachable */ +} diff --git a/toggle.mk b/toggle.mk new file mode 100644 index 0000000..ef09042 --- /dev/null +++ b/toggle.mk @@ -0,0 +1,8 @@ +# spmenu +# See LICENSE file for copyright details. + +# Multi-monitor support. +# Requires libXinerama +# Comment these lines if you don't need it. +XINERAMALIBS = -lXinerama +XINERAMAFLAGS = -DXINERAMA diff --git a/xresources.h b/xresources.h new file mode 100644 index 0000000..013c7a3 --- /dev/null +++ b/xresources.h @@ -0,0 +1,90 @@ +/* This header is for the .Xresources options. + * These will be set on startup by xrdb. + */ + +ResourcePref resources[] = { + { "font", STRING, &font }, + { "caretfgcolor", STRING, &caretfgcolor }, + { "normfgcolor", STRING, &normfgcolor }, + { "normbgcolor", STRING, &normbgcolor }, + { "selfgcolor", STRING, &selfgcolor }, + { "selbgcolor", STRING, &selbgcolor }, + { "numfgcolor", STRING, &numfgcolor }, + { "numbgcolor", STRING, &numbgcolor }, + { "normhlfgcolor", STRING, &normhlfgcolor }, + { "normhlbgcolor", STRING, &normhlbgcolor }, + { "selhlfgcolor", STRING, &selhlfgcolor }, + { "selhlbgcolor", STRING, &selhlbgcolor }, + + /* Pywal support */ + { "color0", STRING, &caretfgcolor }, + { "color10", STRING, &normfgcolor }, + { "color0", STRING, &normbgcolor }, + { "color0", STRING, &selfgcolor }, + { "color6", STRING, &selbgcolor }, + { "color0", STRING, &numfgcolor }, + { "color6", STRING, &numbgcolor }, + { "color2", STRING, &normhlbgcolor }, + { "color3", STRING, &selhlbgcolor }, + { "color3", STRING, &normhlfgcolor }, + { "color0", STRING, &selhlfgcolor }, + + /* sgr colors */ + { "sgrcolor0", STRING, &sgrcolor0 }, + { "sgrcolor1", STRING, &sgrcolor1 }, + { "sgrcolor2", STRING, &sgrcolor2 }, + { "sgrcolor3", STRING, &sgrcolor3 }, + { "sgrcolor4", STRING, &sgrcolor4 }, + { "sgrcolor5", STRING, &sgrcolor5 }, + { "sgrcolor6", STRING, &sgrcolor6 }, + { "sgrcolor7", STRING, &sgrcolor7 }, + { "sgrcolor8", STRING, &sgrcolor8 }, + { "sgrcolor9", STRING, &sgrcolor9 }, + { "sgrcolor10", STRING, &sgrcolor10 }, + { "sgrcolor11", STRING, &sgrcolor11 }, + { "sgrcolor12", STRING, &sgrcolor12 }, + { "sgrcolor13", STRING, &sgrcolor13 }, + { "sgrcolor14", STRING, &sgrcolor14 }, + { "sgrcolor15", STRING, &sgrcolor15 }, + + /* sgr colors */ + { "color0", STRING, &sgrcolor0 }, + { "color1", STRING, &sgrcolor1 }, + { "color2", STRING, &sgrcolor2 }, + { "color3", STRING, &sgrcolor3 }, + { "color4", STRING, &sgrcolor4 }, + { "color5", STRING, &sgrcolor5 }, + { "color6", STRING, &sgrcolor6 }, + { "color7", STRING, &sgrcolor7 }, + { "color8", STRING, &sgrcolor8 }, + { "color9", STRING, &sgrcolor9 }, + { "color10", STRING, &sgrcolor10 }, + { "color11", STRING, &sgrcolor11 }, + { "color12", STRING, &sgrcolor12 }, + { "color13", STRING, &sgrcolor13 }, + { "color14", STRING, &sgrcolor14 }, + { "color15", STRING, &sgrcolor15 }, + + { "menuposition", INTEGER, &menuposition }, + { "menupaddingv", INTEGER, &menupaddingv }, + { "menupaddingh", INTEGER, &menupaddingh }, + + { "alpha", INTEGER, &alpha }, + { "type", INTEGER, &type }, + { "preselected", INTEGER, &preselected }, + { "colorprompt", INTEGER, &colorprompt }, + { "prompt", STRING, &prompt }, + { "class", STRING, &class }, + { "leftarrow", STRING, &leftarrow }, + { "rightarrow", STRING, &rightarrow }, + { "bordercentered", INTEGER, &bordercentered }, + { "borderwidth", INTEGER, &borderwidth }, + { "lines", INTEGER, &lines }, + { "columns", INTEGER, &columns }, + { "lineheight", INTEGER, &lineheight }, + { "minlineheight", INTEGER, &minlineheight }, + { "maxhist", INTEGER, &maxhist }, + { "hidematchcount", INTEGER, &hidematchcount }, + { "histnodup", INTEGER, &histnodup }, + { "casesensitive", INTEGER, &casesensitive }, +};