tmenu

Unnamed repository; edit this file 'description' to name the repository.
git clone https://git.inz.fi/tmenu
Log | Files | Refs | README | LICENSE

commit 8fa055d5969aae5b7c6289a1029823ed94edb9bd
parent 7175c4880bac3d2a2d4a6262b59193f0a38e2fdb
Author: Santtu Lakkala <santtu.lakkala@unikie.com>
Date:   Wed, 11 Mar 2026 10:47:18 +0200

ncurses backend

Diffstat:
MMakefile | 42+++++++++++++++++++++---------------------
MREADME | 12++++++------
Mconfig.def.h | 4++--
Mconfig.mk | 23++++++-----------------
Ddmenu.1 | 204-------------------------------------------------------------------------------
Mdmenu.c | 642++++++++++++++++++++++++++++++++++++++-----------------------------------------
Ddmenu_path | 13-------------
Ddmenu_run | 2--
Mdrw.c | 570++++++++++++++++++++++++++++++++++++++-----------------------------------------
Mdrw.h | 30++++++++++++++++--------------
Mstest.1 | 4++--
Atmenu.1 | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atmenu_path | 13+++++++++++++
Atmenu_run | 2++
14 files changed, 861 insertions(+), 904 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,4 +1,4 @@ -# dmenu - dynamic menu +# tmenu - dynamic menu # See LICENSE file for copyright and license details. include config.mk @@ -6,7 +6,7 @@ include config.mk SRC = drw.c dmenu.c stest.c util.c OBJ = $(SRC:.c=.o) -all: dmenu stest +all: tmenu stest .c.o: $(CC) -c $(CFLAGS) $< @@ -16,43 +16,43 @@ config.h: $(OBJ): arg.h config.h config.mk drw.h -dmenu: dmenu.o drw.o util.o +tmenu: dmenu.o drw.o util.o $(CC) -o $@ dmenu.o drw.o util.o $(LDFLAGS) stest: stest.o $(CC) -o $@ stest.o $(LDFLAGS) clean: - rm -f dmenu stest $(OBJ) dmenu-$(VERSION).tar.gz + rm -f dmenu tmenu stest $(OBJ) dmenu-$(VERSION).tar.gz tmenu-$(VERSION).tar.gz dist: clean - mkdir -p dmenu-$(VERSION) - cp LICENSE Makefile README arg.h config.def.h config.mk dmenu.1\ - drw.h util.h dmenu_path dmenu_run stest.1 $(SRC)\ - dmenu-$(VERSION) - tar -cf dmenu-$(VERSION).tar dmenu-$(VERSION) - gzip dmenu-$(VERSION).tar - rm -rf dmenu-$(VERSION) + mkdir -p tmenu-$(VERSION) + cp LICENSE Makefile README arg.h config.def.h config.mk tmenu.1\ + drw.h util.h tmenu_path tmenu_run stest.1 $(SRC)\ + tmenu-$(VERSION) + tar -cf tmenu-$(VERSION).tar tmenu-$(VERSION) + gzip tmenu-$(VERSION).tar + rm -rf tmenu-$(VERSION) install: all mkdir -p $(DESTDIR)$(PREFIX)/bin - cp -f dmenu dmenu_path dmenu_run stest $(DESTDIR)$(PREFIX)/bin - chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu - chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_path - chmod 755 $(DESTDIR)$(PREFIX)/bin/dmenu_run + cp -f tmenu tmenu_path tmenu_run stest $(DESTDIR)$(PREFIX)/bin + chmod 755 $(DESTDIR)$(PREFIX)/bin/tmenu + chmod 755 $(DESTDIR)$(PREFIX)/bin/tmenu_path + chmod 755 $(DESTDIR)$(PREFIX)/bin/tmenu_run chmod 755 $(DESTDIR)$(PREFIX)/bin/stest mkdir -p $(DESTDIR)$(MANPREFIX)/man1 - sed "s/VERSION/$(VERSION)/g" < dmenu.1 > $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 + sed "s/VERSION/$(VERSION)/g" < tmenu.1 > $(DESTDIR)$(MANPREFIX)/man1/tmenu.1 sed "s/VERSION/$(VERSION)/g" < stest.1 > $(DESTDIR)$(MANPREFIX)/man1/stest.1 - chmod 644 $(DESTDIR)$(MANPREFIX)/man1/dmenu.1 + chmod 644 $(DESTDIR)$(MANPREFIX)/man1/tmenu.1 chmod 644 $(DESTDIR)$(MANPREFIX)/man1/stest.1 uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/dmenu\ - $(DESTDIR)$(PREFIX)/bin/dmenu_path\ - $(DESTDIR)$(PREFIX)/bin/dmenu_run\ + rm -f $(DESTDIR)$(PREFIX)/bin/tmenu\ + $(DESTDIR)$(PREFIX)/bin/tmenu_path\ + $(DESTDIR)$(PREFIX)/bin/tmenu_run\ $(DESTDIR)$(PREFIX)/bin/stest\ - $(DESTDIR)$(MANPREFIX)/man1/dmenu.1\ + $(DESTDIR)$(MANPREFIX)/man1/tmenu.1\ $(DESTDIR)$(MANPREFIX)/man1/stest.1 .PHONY: all clean dist install uninstall diff --git a/README b/README @@ -1,24 +1,24 @@ -dmenu - dynamic menu +tmenu - dynamic menu ==================== -dmenu is an efficient dynamic menu for X. +tmenu is an efficient dynamic menu for X. Requirements ------------ -In order to build dmenu you need the Xlib header files. +In order to build tmenu you need the Xlib header files. Installation ------------ -Edit config.mk to match your local setup (dmenu is installed into +Edit config.mk to match your local setup (tmenu is installed into the /usr/local namespace by default). -Afterwards enter the following command to build and install dmenu +Afterwards enter the following command to build and install tmenu (if necessary as root): make clean install -Running dmenu +Running tmenu ------------- See the man page for details. diff --git a/config.def.h b/config.def.h @@ -1,7 +1,7 @@ /* See LICENSE file for copyright and license details. */ /* Default settings; can be overriden by command line. */ -static int topbar = 1; /* -b option; if 0, dmenu appears at bottom */ +static int topbar = 1; /* -b option; if 0, tmenu appears at bottom */ /* -fn option overrides fonts[0]; default X11 font or font set */ static const char *fonts[] = { "monospace:size=10" @@ -13,7 +13,7 @@ static const char *colors[SchemeLast][2] = { [SchemeSel] = { "#eeeeee", "#005577" }, [SchemeOut] = { "#000000", "#00ffff" }, }; -/* -l option; if nonzero, dmenu uses vertical list with given number of lines */ +/* -l option; if nonzero, tmenu uses vertical list with given number of lines */ static unsigned int lines = 0; /* diff --git a/config.mk b/config.mk @@ -1,30 +1,19 @@ -# dmenu version +# tmenu version VERSION = 5.4 # paths PREFIX = /usr/local MANPREFIX = $(PREFIX)/share/man -X11INC = /usr/X11R6/include -X11LIB = /usr/X11R6/lib - -# Xinerama, comment if you don't want it -XINERAMALIBS = -lXinerama -XINERAMAFLAGS = -DXINERAMA - -# freetype -FREETYPELIBS = -lfontconfig -lXft -FREETYPEINC = /usr/include/freetype2 -# OpenBSD (uncomment) -#FREETYPEINC = $(X11INC)/freetype2 -#MANPREFIX = ${PREFIX}/man +NCURSESCFLAGS = $(filter -I%,$(shell pkg-config --cflags ncursesw)) +NCURSESLIBS = $(shell pkg-config --libs ncursesw) # includes and libs -INCS = -I$(X11INC) -I$(FREETYPEINC) -LIBS = -L$(X11LIB) -lX11 $(XINERAMALIBS) $(FREETYPELIBS) +INCS = $(NCURSESCFLAGS) +LIBS = $(NCURSESLIBS) # flags -CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" $(XINERAMAFLAGS) +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_POSIX_C_SOURCE=200809L -DVERSION=\"$(VERSION)\" CFLAGS = -std=c99 -pedantic -Wall -Os $(INCS) $(CPPFLAGS) LDFLAGS = $(LIBS) diff --git a/dmenu.1 b/dmenu.1 @@ -1,204 +0,0 @@ -.TH DMENU 1 dmenu\-VERSION -.SH NAME -dmenu \- dynamic menu -.SH SYNOPSIS -.B dmenu -.RB [ \-bfiv ] -.RB [ \-l -.IR lines ] -.RB [ \-m -.IR monitor ] -.RB [ \-p -.IR prompt ] -.RB [ \-fn -.IR font ] -.RB [ \-nb -.IR color ] -.RB [ \-nf -.IR color ] -.RB [ \-sb -.IR color ] -.RB [ \-sf -.IR color ] -.RB [ \-ob -.IR color ] -.RB [ \-of -.IR color ] -.RB [ \-w -.IR windowid ] -.P -.BR dmenu_run " ..." -.SH DESCRIPTION -.B dmenu -is a dynamic menu for X, which 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 dmenu terminates. Entering text will narrow the items to those -matching the tokens in the input. -.P -.B dmenu_run -is a script used by -.IR dwm (1) -which lists programs in the user's $PATH and runs the result in their $SHELL. -.SH OPTIONS -.TP -.B \-b -dmenu appears at the bottom of the screen. -.TP -.B \-f -dmenu grabs the keyboard before reading stdin if not reading from a tty. This -is faster, but will lock up X until stdin reaches end\-of\-file. -.TP -.B \-i -dmenu matches menu items case insensitively. -.TP -.BI \-l " lines" -dmenu lists items vertically, with the given number of lines. -.TP -.BI \-m " monitor" -dmenu is displayed on the monitor number supplied. Monitor numbers are starting -from 0. -.TP -.BI \-p " prompt" -defines the prompt to be displayed to the left of the input field. -.TP -.BI \-fn " font" -defines the font or font set used. -.TP -.BI \-nb " color" -defines the normal background color. -.IR #RGB , -.IR #RRGGBB , -and X color names are supported. -.TP -.BI \-nf " color" -defines the normal foreground color. -.TP -.BI \-sb " color" -defines the selected background color. -.TP -.BI \-sf " color" -defines the selected foreground color. -.TP -.BI \-ob " color" -defines the outline background color (for multiple selection). -.TP -.BI \-of " color" -defines the outline foreground color (for multiple selection). -.TP -.B \-v -prints version information to stdout, then exits. -.TP -.BI \-w " windowid" -embed into windowid. -.SH USAGE -dmenu 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 primary X selection -.TP -.B C\-Y -Paste from X clipboard -.TP -.B M\-b -Move cursor to the start of the current word -.TP -.B M\-f -Move cursor to the end of the current word -.TP -.B M\-g -Home -.TP -.B M\-G -End -.TP -.B M\-h -Up -.TP -.B M\-j -Page down -.TP -.B M\-k -Page up -.TP -.B M\-l -Down -.SH SEE ALSO -.IR dwm (1), -.IR stest (1) diff --git a/dmenu.c b/dmenu.c @@ -1,27 +1,28 @@ /* See LICENSE file for copyright and license details. */ #include <ctype.h> +#include <errno.h> +#include <fcntl.h> #include <locale.h> +#include <poll.h> +#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <strings.h> -#include <time.h> +#include <sys/ioctl.h> +#include <term.h> +#include <termios.h> #include <unistd.h> -#include <X11/Xlib.h> -#include <X11/Xatom.h> -#include <X11/Xutil.h> -#ifdef XINERAMA -#include <X11/extensions/Xinerama.h> -#endif -#include <X11/Xft/Xft.h> - #include "drw.h" #include "util.h" +#ifdef lines +#undef lines +#endif + /* 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 ITEMGAP 1 #define TEXTW(X) (drw_fontset_getwidth(drw, (X)) + lrpad) /* enums */ @@ -34,20 +35,20 @@ struct item { }; static char text[BUFSIZ] = ""; -static char *embed; static int bh, mw, mh; static int inputw = 0, promptw; static int lrpad; /* sum of left and right padding */ +static int termh, yoff; +static int passwd; static size_t cursor; static struct item *items = NULL; static struct item *matches, *matchend; static struct item *prev, *curr, *next, *sel; -static int mon = -1, screen; - -static Atom clip, utf8; -static Display *dpy; -static Window root, parentwin, win; -static XIC xic; +static volatile sig_atomic_t resized; +static int ttyfd = -1; +static FILE *ttyout; +static struct termios termios_orig; +static int termios_saved; static Drw *drw; static Clr *scheme[SchemeLast]; @@ -57,13 +58,45 @@ static Clr *scheme[SchemeLast]; static int (*fstrncmp)(const char *, const char *, size_t) = strncmp; static char *(*fstrstr)(const char *, const char *) = strstr; +enum { + XK_Left = 0x101, + XK_Right, + XK_Up, + XK_Down, + XK_Home, + XK_End, + XK_Next, + XK_Prior, + XK_Delete, + XK_BackSpace, + XK_Return, + XK_KP_Left, + XK_KP_Right, + XK_KP_Up, + XK_KP_Down, + XK_KP_Home, + XK_KP_End, + XK_KP_Next, + XK_KP_Prior, + XK_KP_Delete, + XK_KP_Enter, + XK_Escape = 27, + XK_Tab = '\t' +}; + static unsigned int -textw_clamp(const char *str, unsigned int n) +itemw_clamp(const char *str, unsigned int n) { - unsigned int w = drw_fontset_getwidth_clamp(drw, str, n) + lrpad; + unsigned int w = drw_fontset_getwidth_clamp(drw, str, n > ITEMGAP ? n - ITEMGAP : 0) + lrpad + ITEMGAP; return MIN(w, n); } +static const char * +inputtext(void) +{ + return passwd ? "" : text; +} + static void appenditem(struct item *item, struct item **list, struct item **last) { @@ -88,10 +121,10 @@ calcoffsets(void) 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) + if ((i += (lines > 0) ? bh : itemw_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) + if ((i += (lines > 0) ? bh : itemw_clamp(prev->left->text, n)) > n) break; } @@ -100,15 +133,20 @@ cleanup(void) { size_t i; - XUngrabKeyboard(dpy, CurrentTime); for (i = 0; i < SchemeLast; i++) drw_scm_free(drw, scheme[i], 2); for (i = 0; items && items[i].text; ++i) free(items[i].text); free(items); + drw_cursor_restore(); + drw_term_reset(); drw_free(drw); - XSync(dpy, False); - XCloseDisplay(dpy); + if (termios_saved) + tcsetattr(ttyfd, TCSAFLUSH, &termios_orig); + if (ttyout) + fclose(ttyout); + if (ttyfd >= 0) + close(ttyfd); } static char * @@ -132,6 +170,9 @@ cistrstr(const char *h, const char *n) static int drawitem(struct item *item, int x, int y, int w) { + int lpad = lrpad / 2; + int draww = w; + if (item == sel) drw_setscheme(drw, scheme[SchemeSel]); else if (item->out) @@ -139,33 +180,38 @@ drawitem(struct item *item, int x, int y, int w) else drw_setscheme(drw, scheme[SchemeNorm]); - return drw_text(drw, x, y, w, bh, lrpad / 2, item->text, 0); + if (item == sel) { + /* Keep selection background tight: one cell before and after text. */ + lpad = 1; + draww = MIN(w, (int)drw_fontset_getwidth(drw, item->text) + 2); + } + return drw_text(drw, x, y, draww, bh, lpad, item->text, 0) + ITEMGAP; } static void drawmenu(void) { unsigned int curpos; + const char *displaytext; struct item *item; - int x = 0, y = 0, w; + int cursorx, x = 0, y = yoff, w; drw_setscheme(drw, scheme[SchemeNorm]); - drw_rect(drw, 0, 0, mw, mh, 1, 1); + drw_rect(drw, 0, yoff, mw, mh, 1, 0); if (prompt && *prompt) { drw_setscheme(drw, scheme[SchemeSel]); - x = drw_text(drw, x, 0, promptw, bh, lrpad / 2, prompt, 0); + x = drw_text(drw, x, yoff, promptw, bh, lrpad / 2, prompt, 0); } /* draw input field */ w = (lines > 0 || !matches) ? mw - x : inputw; drw_setscheme(drw, scheme[SchemeNorm]); - drw_text(drw, x, 0, w, bh, lrpad / 2, text, 0); + displaytext = inputtext(); + drw_text(drw, x, yoff, w, bh, lrpad / 2, displaytext, 0); - curpos = TEXTW(text) - TEXTW(&text[cursor]); - if ((curpos += lrpad / 2 - 1) < w) { - drw_setscheme(drw, scheme[SchemeNorm]); - drw_rect(drw, x + curpos, 2, 2, bh - 4, 1, 0); - } + curpos = TEXTW(displaytext) - TEXTW(&displaytext[passwd ? 0 : cursor]); + cursorx = x; + curpos += lrpad / 2; if (lines > 0) { /* draw vertical list */ @@ -177,53 +223,19 @@ drawmenu(void) w = TEXTW("<"); if (curr->left) { drw_setscheme(drw, scheme[SchemeNorm]); - drw_text(drw, x, 0, w, bh, lrpad / 2, "<", 0); + drw_text(drw, x, yoff, 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(">"))); + x = drawitem(item, x, yoff, itemw_clamp(item->text, mw - x - TEXTW(">"))); if (next) { w = TEXTW(">"); drw_setscheme(drw, scheme[SchemeNorm]); - drw_text(drw, mw - w, 0, w, bh, lrpad / 2, ">", 0); + drw_text(drw, mw - w, yoff, w, bh, lrpad / 2, ">", 0); } } - 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; - 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) - 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"); + drw_move(drw, MIN(cursorx + (int)curpos, mw - 1), yoff); + drw_map(drw, 0, 0, 0, mw, mh); } static void @@ -231,7 +243,6 @@ match(void) { static char **tokv = NULL; static int tokn = 0; - char buf[sizeof text], *s; int i, tokc = 0; size_t len, textsize; @@ -321,101 +332,181 @@ movewordedge(int dir) } static void -keypress(XKeyEvent *ev) +resize(void) { - char buf[64]; - int len; - KeySym ksym = NoSymbol; - Status status; - - 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 */ + struct winsize ws; + + resized = 0; + if (ioctl(ttyfd, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0 || ws.ws_row == 0) { + mw = 80; + termh = 24; + } else { + mw = ws.ws_col; + termh = ws.ws_row; + } + bh = drw->fonts->h; + mh = lines + 1; + yoff = topbar ? 0 : MAX(termh - mh, 0); + promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0; + inputw = mw / 3; + drw_resize(drw, mw, mh); + calcoffsets(); + drawmenu(); +} + +static void +sigwinch(int unused) +{ + (void)unused; + resized = 1; +} + +static int +readkey(void) +{ + unsigned char ch = 0, seq[3]; + struct pollfd pfd = { .fd = ttyfd, .events = POLLIN }; + int n; + + n = read(ttyfd, &ch, 1); + if (n <= 0) { + if (errno == EINTR && resized) + return 0; + return 0; + } + if (ch == '\r') + return '\n'; + if (ch == 127 || ch == 8) + return XK_BackSpace; + if (ch != 27) + return ch; + + if (poll(&pfd, 1, 25) <= 0) + return 27; + if (read(ttyfd, &seq[0], 1) != 1) + return 27; + + if (seq[0] == '[') { + if (read(ttyfd, &seq[1], 1) != 1) + return 27; + if (seq[1] >= '0' && seq[1] <= '9') { + if (read(ttyfd, &seq[2], 1) != 1) + return 27; + if (seq[2] != '~') + return 27; + switch (seq[1]) { + case '1': return XK_Home; + case '3': return XK_Delete; + case '4': return XK_End; + case '5': return XK_Prior; + case '6': return XK_Next; + case '7': return XK_Home; + case '8': return XK_End; + default: return 27; + } + } + switch (seq[1]) { + case 'A': return XK_Up; + case 'B': return XK_Down; + case 'C': return XK_Right; + case 'D': return XK_Left; + case 'H': return XK_Home; + case 'F': return XK_End; + default: return 27; + } + } + if (seq[0] == 'O') { + if (read(ttyfd, &seq[1], 1) != 1) + return 27; + switch (seq[1]) { + case 'H': return XK_Home; + case 'F': return XK_End; + default: return 27; + } + } + return 0x200 | seq[0]; +} + +static void +keypress(int ch) +{ + char buf[7]; + int len = 0, alt = ch & 0x200; + + ch &= ~0x200; + switch (alt ? ch : 0) { + case 'b': + movewordedge(-1); + goto draw; + case 'f': + movewordedge(+1); + goto draw; + case 'g': + ch = XK_Home; + break; + case 'G': + ch = XK_End; + break; + case 'h': + ch = XK_Up; + break; + case 'j': + ch = XK_Next; + break; + case 'k': + ch = XK_Prior; + break; + case 'l': + ch = XK_Down; + break; + default: 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 */ + if (!alt && ch >= 1 && ch <= 26) { + switch (ch) { + case 1: ch = XK_Home; break; + case 2: ch = XK_Left; break; + case 3: + case 7: + cleanup(); + exit(1); + case 4: ch = XK_Delete; break; + case 5: ch = XK_End; break; + case 6: ch = XK_Right; break; + case 8: ch = XK_BackSpace; break; + case 9: ch = '\t'; break; + case 10: + case 13: ch = '\n'; break; + case 11: text[cursor] = '\0'; match(); - break; - case XK_u: /* delete left */ + goto draw; + case 14: ch = XK_Down; break; + case 16: ch = XK_Up; break; + case 21: insert(NULL, 0 - cursor); - break; - case XK_w: /* delete word */ + goto draw; + case 23: 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; - case XK_y: /* paste selection */ - case XK_Y: - XConvertSelection(dpy, (ev->state & ShiftMask) ? clip : XA_PRIMARY, - utf8, utf8, win, CurrentTime); - return; - 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); + case 25: 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; default: return; } } - switch(ksym) { + switch (ch) { default: -insert: - if (!iscntrl((unsigned char)*buf)) + if (!iscntrl((unsigned char)ch)) { + buf[0] = ch; + len = 1; insert(buf, len); + } break; case XK_Delete: case XK_KP_Delete: @@ -424,6 +515,7 @@ insert: cursor = nextrune(+1); /* fallthrough */ case XK_BackSpace: + case 127: if (cursor == 0) return; insert(NULL, nextrune(-1) - cursor); @@ -487,16 +579,24 @@ insert: sel = curr = prev; calcoffsets(); break; + case '\n': case XK_Return: case XK_KP_Enter: - puts((sel && !(ev->state & ShiftMask)) ? sel->text : text); - if (!(ev->state & ControlMask)) { + if (sel && sel->text) + buf[0] = '\0'; + if (sel && sel->text) { + char *s; + + if (!(s = strdup(sel->text))) + die("strdup:"); cleanup(); + puts(s); + free(s); exit(0); } - if (sel) - sel->out = 1; - break; + cleanup(); + puts(text); + exit(0); case XK_Right: case XK_KP_Right: if (text[cursor] != '\0') { @@ -528,24 +628,6 @@ draw: } 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 readstdin(void) { char *line = NULL; @@ -563,7 +645,6 @@ readstdin(void) line[len - 1] = '\0'; if (!(items[i].text = strdup(line))) die("strdup:"); - items[i].out = 0; } free(line); @@ -575,164 +656,77 @@ readstdin(void) 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 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; - } + drawmenu(); + for (;;) { + if (resized) + resize(); + keypress(readkey()); } } static void setup(void) { - int x, y, i, j; - unsigned int du; - XSetWindowAttributes swa; - XIM xim; - Window w, dw, *dws; - XWindowAttributes wa; - XClassHint ch = {"dmenu", "dmenu"}; -#ifdef XINERAMA - XineramaScreenInfo *info; - Window pw; - int a, di, n, area = 0; -#endif + int i, termerr; + struct sigaction sa; + struct termios raw; + + ttyfd = open("/dev/tty", O_RDWR | O_CLOEXEC); + if (ttyfd < 0) + ttyfd = dup(STDERR_FILENO); + if (ttyfd < 0) + die("open /dev/tty:"); + if (!(ttyout = fdopen(dup(ttyfd), "w"))) + die("fdopen:"); + if (setupterm(NULL, ttyfd, &termerr) == -1 || termerr <= 0) + die("setupterm failed"); + drw_term_init(ttyout); + + if (tcgetattr(ttyfd, &termios_orig) == -1) + die("tcgetattr:"); + termios_saved = 1; + raw = termios_orig; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + raw.c_cc[VMIN] = 1; + raw.c_cc[VTIME] = 0; + if (tcsetattr(ttyfd, TCSAFLUSH, &raw) == -1) + die("tcsetattr:"); + /* init appearance */ - for (j = 0; j < SchemeLast; j++) - scheme[j] = drw_scm_create(drw, colors[j], 2); - - clip = XInternAtom(dpy, "CLIPBOARD", False); - utf8 = XInternAtom(dpy, "UTF8_STRING", False); - - /* calculate menu geometry */ - bh = drw->fonts->h + 2; - lines = MAX(lines, 0); - mh = (lines + 1) * bh; -#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]) != 0) - break; - - x = info[i].x_org; - y = info[i].y_org + (topbar ? 0 : info[i].height - mh); - mw = info[i].width; - XFree(info); - } else -#endif - { - if (!XGetWindowAttributes(dpy, parentwin, &wa)) - die("could not get embedding window attributes: 0x%lx", - parentwin); - x = 0; - y = topbar ? 0 : wa.height - mh; - mw = wa.width; - } - promptw = (prompt && *prompt) ? TEXTW(prompt) - lrpad / 4 : 0; - inputw = mw / 3; /* input width: ~33% of monitor width */ - match(); + for (i = 0; i < SchemeLast; i++) + scheme[i] = drw_scm_create(drw, colors[i], 2); - /* create menu window */ - swa.override_redirect = True; - swa.background_pixel = scheme[SchemeNorm][ColBg].pixel; - swa.event_mask = ExposureMask | KeyPressMask | VisibilityChangeMask; - win = XCreateWindow(dpy, root, x, y, mw, mh, 0, - CopyFromParent, CopyFromParent, CopyFromParent, - CWOverrideRedirect | CWBackPixel | CWEventMask, &swa); - 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) { - XReparentWindow(dpy, win, parentwin, x, y); - 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(); + memset(&sa, 0, sizeof sa); + sa.sa_handler = sigwinch; + sigemptyset(&sa.sa_mask); + sigaction(SIGWINCH, &sa, NULL); + match(); + drw_cursor_save(); + resize(); } static void usage(void) { - die("usage: dmenu [-bfiv] [-l lines] [-p prompt] [-fn font] [-m monitor]\n" - " [-nb color] [-nf color] [-sb color] [-sf color] [-w windowid]"); + die("usage: tmenu [-biv] [-l lines] [-p prompt]\n" + " [-nb color] [-nf color] [-sb color] [-sf color]"); } int main(int argc, char *argv[]) { - XWindowAttributes wa; - int i, fast = 0; + int i; for (i = 1; i < argc; i++) /* these options take no arguments */ if (!strcmp(argv[i], "-v")) { /* prints version information */ - puts("dmenu-"VERSION); + puts("tmenu-"VERSION); exit(0); } else if (!strcmp(argv[i], "-b")) /* appears at the bottom of the screen */ topbar = 0; - else if (!strcmp(argv[i], "-f")) /* grabs keyboard before reading stdin */ - fast = 1; else if (!strcmp(argv[i], "-i")) { /* case-insensitive item matching */ fstrncmp = strncasecmp; fstrstr = cistrstr; @@ -741,8 +735,6 @@ main(int argc, char *argv[]) /* these options take one argument */ else if (!strcmp(argv[i], "-l")) /* number of lines in vertical list */ lines = atoi(argv[++i]); - else if (!strcmp(argv[i], "-m")) - mon = 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 */ @@ -759,39 +751,29 @@ main(int argc, char *argv[]) colors[SchemeOut][ColBg] = argv[++i]; else if (!strcmp(argv[i], "-of")) /* outline foreground color */ colors[SchemeOut][ColFg] = argv[++i]; - else if (!strcmp(argv[i], "-w")) /* embedding window id */ - embed = argv[++i]; + else if (!strcmp(argv[i], "-f") || !strcmp(argv[i], "-m") || !strcmp(argv[i], "-w")) /* legacy compatibility */ + if (strcmp(argv[i], "-f") && ++i == argc) + usage(); + else + continue; else usage(); - if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + if (!setlocale(LC_CTYPE, "")) 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); - drw = drw_create(dpy, screen, root, wa.width, wa.height); + passwd = !strcmp(colors[SchemeNorm][ColFg], colors[SchemeNorm][ColBg]); + + drw = drw_create(NULL, 0, 0, 0, 0); if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) die("no fonts could be loaded."); lrpad = drw->fonts->h; #ifdef __OpenBSD__ - if (pledge("stdio rpath", NULL) == -1) + if (pledge("stdio rpath tty", NULL) == -1) die("pledge"); #endif - if (fast && !isatty(0)) { - grabkeyboard(); - readstdin(); - } else { - readstdin(); - grabkeyboard(); - } + readstdin(); setup(); run(); diff --git a/dmenu_path b/dmenu_path @@ -1,13 +0,0 @@ -#!/bin/sh - -cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}" -cache="$cachedir/dmenu_run" - -[ ! -e "$cachedir" ] && mkdir -p "$cachedir" - -IFS=: -if stest -dqr -n "$cache" $PATH; then - stest -flx $PATH | sort -u | tee "$cache" -else - cat "$cache" -fi diff --git a/dmenu_run b/dmenu_run @@ -1,2 +0,0 @@ -#!/bin/sh -dmenu_path | dmenu "$@" | ${SHELL:-"/bin/sh"} & diff --git a/drw.c b/drw.c @@ -1,15 +1,48 @@ /* See LICENSE file for copyright and license details. */ +#include <ctype.h> +#include <limits.h> #include <stdio.h> #include <stdlib.h> #include <string.h> -#include <X11/Xlib.h> -#include <X11/Xft/Xft.h> +#include <strings.h> +#include <term.h> +#include <wchar.h> #include "drw.h" #include "util.h" #define UTF_INVALID 0xFFFD +static FILE *ttyout; +static char *cap_cup; +static char *cap_sgr0; +static char *cap_setaf; +static char *cap_setab; +static char *cap_sc; +static char *cap_rc; + +static int +ttyputc(int c) +{ + return ttyout ? fputc(c, ttyout) : EOF; +} + +static void +putcap(char *cap) +{ + if (cap) + tputs(cap, 1, ttyputc); +} + +static void +setcolor(short fg, short bg) +{ + if (cap_setaf && fg >= 0) + putcap(tparm(cap_setaf, fg)); + if (cap_setab && bg >= 0) + putcap(tparm(cap_setab, bg)); +} + static int utf8decode(const char *s_in, long *u, int *err) { @@ -23,16 +56,17 @@ utf8decode(const char *s_in, long *u, int *err) }; static const unsigned char leading_mask[] = { 0x7F, 0x1F, 0x0F, 0x07 }; static const unsigned int overlong[] = { 0x0, 0x80, 0x0800, 0x10000 }; - const unsigned char *s = (const unsigned char *)s_in; - int len = lens[*s >> 3]; + int len = lens[*s >> 3], i; + long cp; + *u = UTF_INVALID; *err = 1; if (len == 0) return 1; - long cp = s[0] & leading_mask[len - 1]; - for (int i = 1; i < len; ++i) { + cp = s[0] & leading_mask[len - 1]; + for (i = 1; i < len; ++i) { if (s[i] == '\0' || (s[i] & 0xC0) != 0x80) return i; cp = (cp << 6) | (s[i] & 0x3F); @@ -46,20 +80,112 @@ utf8decode(const char *s_in, long *u, int *err) return len; } +static short +rgb2color(int r, int g, int b) +{ + static const struct { short id; int r, g, b; } map[] = { + { 0, 0x00, 0x00, 0x00 }, + { 1, 0xcd, 0x00, 0x00 }, + { 2, 0x00, 0xcd, 0x00 }, + { 3, 0xcd, 0xcd, 0x00 }, + { 4, 0x00, 0x00, 0xee }, + { 5, 0xcd, 0x00, 0xcd }, + { 6, 0x00, 0xcd, 0xcd }, + { 7, 0xe5, 0xe5, 0xe5 }, + }; + int best = 0, bestdist = INT_MAX, i, dist; + + for (i = 0; i < LENGTH(map); i++) { + dist = (r - map[i].r) * (r - map[i].r) + + (g - map[i].g) * (g - map[i].g) + + (b - map[i].b) * (b - map[i].b); + if (dist < bestdist) { + best = i; + bestdist = dist; + } + } + return map[best].id; +} + +static short +parsecolor(const char *clrname) +{ + unsigned int r, g, b; + + if (!clrname || !*clrname) + return -1; + if (clrname[0] == '#' && strlen(clrname) == 7 + && sscanf(clrname + 1, "%02x%02x%02x", &r, &g, &b) == 3) + return rgb2color(r, g, b); + if (!strcasecmp(clrname, "black")) + return 0; + if (!strcasecmp(clrname, "red")) + return 1; + if (!strcasecmp(clrname, "green")) + return 2; + if (!strcasecmp(clrname, "yellow")) + return 3; + if (!strcasecmp(clrname, "blue")) + return 4; + if (!strcasecmp(clrname, "magenta")) + return 5; + if (!strcasecmp(clrname, "cyan")) + return 6; + if (!strcasecmp(clrname, "white")) + return 7; + return -1; +} + +static int +textwidth(const char *text, unsigned int *bytes, unsigned int limit) +{ + int width = 0, err, len, rw; + long cp; + unsigned int used = 0; + + if (!text) + return 0; + while (*text) { + len = utf8decode(text, &cp, &err); + rw = err ? 1 : wcwidth((wchar_t)cp); + if (rw < 0) + rw = 1; + if (limit && width + rw > (int)limit) + break; + width += rw; + used += len; + text += len; + } + if (bytes) + *bytes = used; + return width; +} + +static void +move_cursor(int x, int y) +{ + if (!cap_cup) + return; + putcap(tparm(cap_cup, y, x)); +} + +static void +fill_spaces(unsigned int n) +{ + while (n-- > 0) + ttyputc(' '); +} + Drw * -drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h) +drw_create(void *unused1, int unused2, unsigned long unused3, unsigned int w, unsigned int h) { Drw *drw = ecalloc(1, sizeof(Drw)); - drw->dpy = dpy; - drw->screen = screen; - drw->root = root; + (void)unused1; + (void)unused2; + (void)unused3; drw->w = w; drw->h = h; - drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen)); - drw->gc = XCreateGC(dpy, root, 0, NULL); - XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); - return drw; } @@ -68,141 +194,68 @@ 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, DefaultDepth(drw->dpy, drw->screen)); } void drw_free(Drw *drw) { - XFreePixmap(drw->dpy, drw->drawable); - XFreeGC(drw->dpy, drw->gc); + if (!drw) + return; drw_fontset_free(drw->fonts); free(drw); } -/* This function is an implementation detail. Library users should use - * drw_fontset_create instead. - */ -static Fnt * -xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern) +Fnt * +drw_fontset_create(Drw *drw, const char *fonts[], size_t fontcount) { Fnt *font; - XftFont *xfont = NULL; - FcPattern *pattern = NULL; - - if (fontname) { - /* Using the pattern found at font->xfont->pattern does not yield the - * same substitution results as using the pattern returned by - * FcNameParse; using the latter results in the desired fallback - * behaviour whereas the former just results in missing-character - * rectangles being drawn, at least with some fonts. */ - if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) { - fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname); - return NULL; - } - if (!(pattern = FcNameParse((FcChar8 *) fontname))) { - fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname); - XftFontClose(drw->dpy, xfont); - return NULL; - } - } else if (fontpattern) { - if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) { - fprintf(stderr, "error, cannot load font from pattern.\n"); - return NULL; - } - } else { - die("no font specified."); - } - - font = ecalloc(1, sizeof(Fnt)); - font->xfont = xfont; - font->pattern = pattern; - font->h = xfont->ascent + xfont->descent; - font->dpy = drw->dpy; - - return font; -} - -static void -xfont_free(Fnt *font) -{ - if (!font) - return; - if (font->pattern) - FcPatternDestroy(font->pattern); - XftFontClose(font->dpy, font->xfont); - free(font); -} -Fnt* -drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount) -{ - Fnt *cur, *ret = NULL; - size_t i; - - if (!drw || !fonts) + if (!drw || !fontcount || !fonts) return NULL; - - for (i = 1; i <= fontcount; i++) { - if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) { - cur->next = ret; - ret = cur; - } - } - return (drw->fonts = ret); + font = ecalloc(1, sizeof(Fnt)); + font->h = 1; + return drw->fonts = font; } void drw_fontset_free(Fnt *font) { - if (font) { - drw_fontset_free(font->next); - xfont_free(font); - } + free(font); } void drw_clr_create(Drw *drw, Clr *dest, const char *clrname) { - if (!drw || !dest || !clrname) - return; - - if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen), - DefaultColormap(drw->dpy, drw->screen), - clrname, dest)) - die("error, cannot allocate color '%s'", clrname); + (void)drw; + dest->fg = parsecolor(clrname); + dest->bg = -1; + dest->pair = 0; } -/* Create color schemes. */ Clr * drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount) { - size_t i; Clr *ret; + size_t i; - /* need at least two colors for a scheme */ - if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(Clr)))) + if (!drw || !clrnames || clrcount < 2) return NULL; - + ret = ecalloc(clrcount, sizeof(Clr)); for (i = 0; i < clrcount; i++) drw_clr_create(drw, &ret[i], clrnames[i]); + ret[ColBg].bg = ret[ColBg].fg; + ret[ColFg].pair = 0; + ret[ColBg].pair = 0; return ret; } void drw_clr_free(Drw *drw, Clr *c) { - if (!drw || !c) - return; - - /* c is typedef XftColor Clr */ - XftColorFree(drw->dpy, DefaultVisual(drw->dpy, drw->screen), - DefaultColormap(drw->dpy, drw->screen), c); + (void)drw; + (void)c; } void @@ -212,7 +265,6 @@ drw_scm_free(Drw *drw, Clr *scm, size_t clrcount) if (!drw || !scm) return; - for (i = 0; i < clrcount; i++) drw_clr_free(drw, &scm[i]); free(scm); @@ -233,186 +285,123 @@ drw_setscheme(Drw *drw, Clr *scm) } void -drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +drw_term_init(FILE *out) { - 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); + ttyout = out; + cap_cup = tigetstr("cup"); + cap_sgr0 = tigetstr("sgr0"); + cap_setaf = tigetstr("setaf"); + cap_setab = tigetstr("setab"); + cap_sc = tigetstr("sc"); + cap_rc = tigetstr("rc"); + if (cap_cup == (char *)-1) + cap_cup = NULL; + if (cap_sgr0 == (char *)-1) + cap_sgr0 = NULL; + if (cap_setaf == (char *)-1) + cap_setaf = NULL; + if (cap_setab == (char *)-1) + cap_setab = NULL; + if (cap_sc == (char *)-1) + cap_sc = NULL; + if (cap_rc == (char *)-1) + cap_rc = NULL; } -int -drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +void +drw_term_reset(void) { - int ty, ellipsis_x = 0; - unsigned int tmpw, ew, ellipsis_w = 0, ellipsis_len, hash, h0, h1; - XftDraw *d = NULL; - Fnt *usedfont, *curfont, *nextfont; - int utf8strlen, utf8charlen, utf8err, render = x || y || w || h; - long utf8codepoint = 0; - const char *utf8str; - FcCharSet *fccharset; - FcPattern *fcpattern; - FcPattern *match; - XftResult result; - int charexists = 0, overflow = 0; - /* keep track of a couple codepoints for which we have no match. */ - static unsigned int nomatches[128], ellipsis_width, invalid_width; - static const char invalid[] = "�"; - - if (!drw || (render && (!drw->scheme || !w)) || !text || !drw->fonts) - return 0; + putcap(cap_sgr0); + if (ttyout) + fflush(ttyout); +} - if (!render) { - w = invert ? invert : ~invert; - } else { - XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); - XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); - if (w < lpad) - return x + w; - d = XftDrawCreate(drw->dpy, drw->drawable, - DefaultVisual(drw->dpy, drw->screen), - DefaultColormap(drw->dpy, drw->screen)); - x += lpad; - w -= lpad; - } +void +drw_cursor_save(void) +{ + if (cap_sc) + putcap(cap_sc); + else if (ttyout) + fputs("\0337", ttyout); +} - usedfont = drw->fonts; - if (!ellipsis_width && render) - ellipsis_width = drw_fontset_getwidth(drw, "..."); - if (!invalid_width && render) - invalid_width = drw_fontset_getwidth(drw, invalid); - while (1) { - ew = ellipsis_len = utf8err = utf8charlen = utf8strlen = 0; - utf8str = text; - nextfont = NULL; - while (*text) { - utf8charlen = utf8decode(text, &utf8codepoint, &utf8err); - for (curfont = drw->fonts; curfont; curfont = curfont->next) { - charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); - if (charexists) { - drw_font_getexts(curfont, text, utf8charlen, &tmpw, NULL); - if (ew + ellipsis_width <= w) { - /* keep track where the ellipsis still fits */ - ellipsis_x = x + ew; - ellipsis_w = w - ew; - ellipsis_len = utf8strlen; - } - - if (ew + tmpw > w) { - overflow = 1; - /* called from drw_fontset_getwidth_clamp(): - * it wants the width AFTER the overflow - */ - if (!render) - x += tmpw; - else - utf8strlen = ellipsis_len; - } else if (curfont == usedfont) { - text += utf8charlen; - utf8strlen += utf8err ? 0 : utf8charlen; - ew += utf8err ? 0 : tmpw; - } else { - nextfont = curfont; - } - break; - } - } - - if (overflow || !charexists || nextfont || utf8err) - break; - else - charexists = 0; - } +void +drw_cursor_restore(void) +{ + if (cap_rc) + putcap(cap_rc); + else if (ttyout) + fputs("\0338", ttyout); +} - if (utf8strlen) { - if (render) { - ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent; - XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg], - usedfont->xfont, x, ty, (XftChar8 *)utf8str, utf8strlen); - } - x += ew; - w -= ew; - } - if (utf8err && (!render || invalid_width < w)) { - if (render) - drw_text(drw, x, y, w, h, 0, invalid, invert); - x += invalid_width; - w -= invalid_width; - } - if (render && overflow) - drw_text(drw, ellipsis_x, y, ellipsis_w, h, 0, "...", invert); +void +drw_move(Drw *drw, int x, int y) +{ + (void)drw; + move_cursor(x, y); +} - if (!*text || overflow) { - break; - } else if (nextfont) { - charexists = 0; - usedfont = nextfont; - } else { - /* Regardless of whether or not a fallback font is found, the - * character must be drawn. */ - charexists = 1; - - hash = (unsigned int)utf8codepoint; - hash = ((hash >> 16) ^ hash) * 0x21F0AAAD; - hash = ((hash >> 15) ^ hash) * 0xD35A2D97; - h0 = ((hash >> 15) ^ hash) % LENGTH(nomatches); - h1 = (hash >> 17) % LENGTH(nomatches); - /* avoid expensive XftFontMatch call when we know we won't find a match */ - if (nomatches[h0] == utf8codepoint || nomatches[h1] == utf8codepoint) - goto no_match; - - fccharset = FcCharSetCreate(); - FcCharSetAddChar(fccharset, utf8codepoint); - - if (!drw->fonts->pattern) { - /* Refer to the comment in xfont_create for more information. */ - die("the first font in the cache must be loaded from a font string."); - } - - fcpattern = FcPatternDuplicate(drw->fonts->pattern); - FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset); - FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue); - - FcConfigSubstitute(NULL, fcpattern, FcMatchPattern); - FcDefaultSubstitute(fcpattern); - match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result); - - FcCharSetDestroy(fccharset); - FcPatternDestroy(fcpattern); - - if (match) { - usedfont = xfont_create(drw, NULL, match); - if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) { - for (curfont = drw->fonts; curfont->next; curfont = curfont->next) - ; /* NOP */ - curfont->next = usedfont; - } else { - xfont_free(usedfont); - nomatches[nomatches[h0] ? h1 : h0] = utf8codepoint; -no_match: - usedfont = drw->fonts; - } - } - } +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + unsigned int i; + short fg, bg; + + if (!drw || !drw->scheme || !filled) + return; + fg = invert ? drw->scheme[ColBg].fg : drw->scheme[ColFg].fg; + bg = invert ? drw->scheme[ColFg].fg : drw->scheme[ColBg].fg; + setcolor(fg, bg); + for (i = 0; i < h; i++) { + move_cursor(x, y + (int)i); + fill_spaces(w); } - if (d) - XftDrawDestroy(d); +} - return x + (render ? w : 0); +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +{ + unsigned int i, bytes = 0, avail; + int width; + short fg, bg; + + if (!drw || !text) + return x; + if (!w) + return textwidth(text, NULL, invert ? (unsigned int)invert : 0); + + fg = invert ? drw->scheme[ColBg].fg : drw->scheme[ColFg].fg; + bg = invert ? drw->scheme[ColFg].fg : drw->scheme[ColBg].fg; + setcolor(fg, bg); + for (i = 0; i < h; i++) { + move_cursor(x, y + (int)i); + fill_spaces(w); + } + if (w < lpad) + return x + w; + + x += lpad; + avail = w - lpad; + width = textwidth(text, &bytes, avail); + if (bytes) { + move_cursor(x, y + (int)h / 2); + fwrite(text, 1, bytes, ttyout); + } + return x + width; } void -drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +drw_map(Drw *drw, unsigned long 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); + (void)drw; + (void)win; + (void)x; + (void)y; + (void)w; + (void)h; + putcap(cap_sgr0); + if (ttyout) + fflush(ttyout); } unsigned int @@ -420,31 +409,31 @@ drw_fontset_getwidth(Drw *drw, const char *text) { if (!drw || !drw->fonts || !text) return 0; - return drw_text(drw, 0, 0, 0, 0, 0, text, 0); + return textwidth(text, NULL, 0); } unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n) { - unsigned int tmp = 0; - if (drw && drw->fonts && text && n) - tmp = drw_text(drw, 0, 0, 0, 0, 0, text, n); - return MIN(n, tmp); + if (!drw || !drw->fonts || !text) + return 0; + return textwidth(text, NULL, n); } void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h) { - XGlyphInfo ext; + char *buf; if (!font || !text) return; - - XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext); + buf = ecalloc(len + 1, sizeof(char)); + memcpy(buf, text, len); if (w) - *w = ext.xOff; + *w = textwidth(buf, NULL, 0); if (h) *h = font->h; + free(buf); } Cur * @@ -452,20 +441,15 @@ drw_cur_create(Drw *drw, int shape) { Cur *cur; - if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) - return NULL; - - cur->cursor = XCreateFontCursor(drw->dpy, shape); - + (void)drw; + cur = ecalloc(1, sizeof(Cur)); + cur->cursor = shape; return cur; } void drw_cur_free(Drw *drw, Cur *cursor) { - if (!cursor) - return; - - XFreeCursor(drw->dpy, cursor->cursor); + (void)drw; free(cursor); } diff --git a/drw.h b/drw.h @@ -1,39 +1,36 @@ /* See LICENSE file for copyright and license details. */ +#include <stddef.h> +#include <stdio.h> typedef struct { - Cursor cursor; + int cursor; } Cur; typedef struct Fnt { - Display *dpy; unsigned int h; - XftFont *xfont; - FcPattern *pattern; struct Fnt *next; } Fnt; enum { ColFg, ColBg }; /* Clr scheme index */ -typedef XftColor Clr; +typedef struct { + short fg, bg; + short pair; +} Clr; typedef struct { unsigned int w, h; - Display *dpy; - int screen; - Window root; - Drawable drawable; - GC gc; Clr *scheme; Fnt *fonts; } Drw; /* Drawable abstraction */ -Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h); +Drw *drw_create(void *unused1, int unused2, unsigned long unused3, unsigned int w, unsigned int h); void drw_resize(Drw *drw, unsigned int w, unsigned int h); void drw_free(Drw *drw); /* Fnt abstraction */ -Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount); -void drw_fontset_free(Fnt* set); +Fnt *drw_fontset_create(Drw *drw, const char *fonts[], size_t fontcount); +void drw_fontset_free(Fnt *set); unsigned int drw_fontset_getwidth(Drw *drw, const char *text); unsigned int drw_fontset_getwidth_clamp(Drw *drw, const char *text, unsigned int n); void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h); @@ -51,10 +48,15 @@ void drw_cur_free(Drw *drw, Cur *cursor); /* Drawing context manipulation */ void drw_setfontset(Drw *drw, Fnt *set); void drw_setscheme(Drw *drw, Clr *scm); +void drw_term_init(FILE *out); +void drw_term_reset(void); +void drw_cursor_save(void); +void drw_cursor_restore(void); +void drw_move(Drw *drw, int x, int y); /* 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); /* Map functions */ -void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); +void drw_map(Drw *drw, unsigned long win, int x, int y, unsigned int w, unsigned int h); diff --git a/stest.1 b/stest.1 @@ -1,4 +1,4 @@ -.TH STEST 1 dmenu\-VERSION +.TH STEST 1 tmenu\-VERSION .SH NAME stest \- filter a list of files by properties .SH SYNOPSIS @@ -86,5 +86,5 @@ No files passed all tests. .B 2 An error occurred. .SH SEE ALSO -.IR dmenu (1), +.IR tmenu (1), .IR test (1) diff --git a/tmenu.1 b/tmenu.1 @@ -0,0 +1,204 @@ +.TH TMENU 1 tmenu\-VERSION +.SH NAME +tmenu \- dynamic menu +.SH SYNOPSIS +.B tmenu +.RB [ \-bfiv ] +.RB [ \-l +.IR lines ] +.RB [ \-m +.IR monitor ] +.RB [ \-p +.IR prompt ] +.RB [ \-fn +.IR font ] +.RB [ \-nb +.IR color ] +.RB [ \-nf +.IR color ] +.RB [ \-sb +.IR color ] +.RB [ \-sf +.IR color ] +.RB [ \-ob +.IR color ] +.RB [ \-of +.IR color ] +.RB [ \-w +.IR windowid ] +.P +.BR tmenu_run " ..." +.SH DESCRIPTION +.B tmenu +is a dynamic menu for X, which 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 tmenu terminates. Entering text will narrow the items to those +matching the tokens in the input. +.P +.B tmenu_run +is a script used by +.IR dwm (1) +which lists programs in the user's $PATH and runs the result in their $SHELL. +.SH OPTIONS +.TP +.B \-b +tmenu appears at the bottom of the screen. +.TP +.B \-f +tmenu grabs the keyboard before reading stdin if not reading from a tty. This +is faster, but will lock up X until stdin reaches end\-of\-file. +.TP +.B \-i +tmenu matches menu items case insensitively. +.TP +.BI \-l " lines" +tmenu lists items vertically, with the given number of lines. +.TP +.BI \-m " monitor" +tmenu is displayed on the monitor number supplied. Monitor numbers are starting +from 0. +.TP +.BI \-p " prompt" +defines the prompt to be displayed to the left of the input field. +.TP +.BI \-fn " font" +defines the font or font set used. +.TP +.BI \-nb " color" +defines the normal background color. +.IR #RGB , +.IR #RRGGBB , +and X color names are supported. +.TP +.BI \-nf " color" +defines the normal foreground color. +.TP +.BI \-sb " color" +defines the selected background color. +.TP +.BI \-sf " color" +defines the selected foreground color. +.TP +.BI \-ob " color" +defines the outline background color (for multiple selection). +.TP +.BI \-of " color" +defines the outline foreground color (for multiple selection). +.TP +.B \-v +prints version information to stdout, then exits. +.TP +.BI \-w " windowid" +embed into windowid. +.SH USAGE +tmenu 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 primary X selection +.TP +.B C\-Y +Paste from X clipboard +.TP +.B M\-b +Move cursor to the start of the current word +.TP +.B M\-f +Move cursor to the end of the current word +.TP +.B M\-g +Home +.TP +.B M\-G +End +.TP +.B M\-h +Up +.TP +.B M\-j +Page down +.TP +.B M\-k +Page up +.TP +.B M\-l +Down +.SH SEE ALSO +.IR dwm (1), +.IR stest (1) diff --git a/tmenu_path b/tmenu_path @@ -0,0 +1,13 @@ +#!/bin/sh + +cachedir="${XDG_CACHE_HOME:-"$HOME/.cache"}" +cache="$cachedir/tmenu_run" + +[ ! -e "$cachedir" ] && mkdir -p "$cachedir" + +IFS=: +if stest -dqr -n "$cache" $PATH; then + stest -flx $PATH | sort -u | tee "$cache" +else + cat "$cache" +fi diff --git a/tmenu_run b/tmenu_run @@ -0,0 +1,2 @@ +#!/bin/sh +tmenu_path | tmenu "$@" | ${SHELL:-"/bin/sh"}