thingmenu

A simple graphical menu launcher for X11.
git clone git://r-36.net/thingmenu
Log | Files | Refs | LICENSE

commit 338b4a391a7991d938921284fe500afeceab1a1b
Author: Christoph Lohmann <20h@r-36.net>
Date:   Sun, 27 Mar 2011 22:10:02 +0200

Initial commit.

Diffstat:
LICENSE | 22++++++++++++++++++++++
Makefile | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
README.md | 26++++++++++++++++++++++++++
config.def.h | 7+++++++
config.h | 7+++++++
config.mk | 26++++++++++++++++++++++++++
thingmenu.c | 555+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 703 insertions(+), 0 deletions(-)

diff --git a/LICENSE b/LICENSE @@ -0,0 +1,22 @@ +MIT/X Consortium License + +© 2011 Christoph Lohmann <20h@r-36.net> + +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 @@ -0,0 +1,60 @@ +# thinglaunch +# See LICENSE file for copyright and license details. + +include config.mk + +SRC = ${NAME}.c + +OBJ = ${SRC:.c=.o} + +all: options ${NAME} + +options: + @echo ${NAME} build options: + @echo "CFLAGS = ${CFLAGS}" + @echo "LDFLAGS = ${LDFLAGS}" + @echo "CC = ${CC}" + +config.h: config.mk + @echo creating $@ from config.def.h + @cp config.def.h $@ + +.c.o: + @echo CC $< + @${CC} -c ${CFLAGS} $< + +${OBJ}: config.mk + +${NAME}: ${OBJ} + @echo CC -o $@ + @${CC} -o $@ ${OBJ} ${LDFLAGS} + +clean: + @echo cleaning + @rm -f ${NAME} ${OBJ} ${NAME}-${VERSION}.tar.gz + +dist: clean + @echo creating dist tarball + @mkdir -p ${NAME}-${VERSION} + @cp -R LICENSE Makefile README.md config.mk \ + ${SRC} *.h ${NAME}-${VERSION} + @tar -cf ${NAME}-${VERSION}.tar ${NAME}-${VERSION} + @gzip ${NAME}-${VERSION}.tar + @rm -rf ${NAME}-${VERSION} + +etc: + @echo installing etc files into ${DESTDIR}/etc/${NAME} + @mkdir -p ${DESTDIR}/etc/${NAME} + @cp -R etc/${NAME}/* ${DESTDIR}/etc/${NAME} + +install: all + @echo installing executable file to ${DESTDIR}${PREFIX}/bin + @mkdir -p ${DESTDIR}${PREFIX}/bin + @cp -f ${NAME} ${DESTDIR}${PREFIX}/bin + @chmod 755 ${DESTDIR}${PREFIX}/bin/${NAME} + +uninstall: + @echo removing executable file from ${DESTDIR}${PREFIX}/bin + @rm -f ${DESTDIR}${PREFIX}/bin/${NAME} + +.PHONY: all options clean dist install uninstall diff --git a/README.md b/README.md @@ -0,0 +1,26 @@ +# Thingmenu - a simple X11 menu + +This application evolved out of the need to be able to run commands +in a touchscreen environment. + +## Installation + + % tar -xzvf thingmenu-*.tar.gz + % cd thingmenu + % make + % sudo PREFIX=/usr make install + +## Usage + + # This will open a 300px wide menu, which is showing an + # entry "Reboot now". When being clicked this entry will run + # "reboot". After that the menu will not exit (-s). + % thingmenu -s -ww 300 -- "Reboot now:reboot" + + # This will create a centered menu, which is aligned based + # on the length of the label texts. After the first clicked + # entry it will exit. + % thingmenu "Force reboot:reboot -f" "Shutdown:shutdown" + +Have fun! + diff --git a/config.def.h b/config.def.h @@ -0,0 +1,7 @@ +static const Bool wmborder = True; +static const char *font = "-*-*-medium-*-*-*-14-*-*-*-*-*-*-*"; +static const char *normbgcolor = "#222222"; +static const char *normfgcolor = "#cccccc"; +static const char *pressbgcolor = "#ffffff"; +static const char *pressfgcolor = "#555555"; + diff --git a/config.h b/config.h @@ -0,0 +1,7 @@ +static const Bool wmborder = True; +static const char *font = "-*-*-medium-*-*-*-14-*-*-*-*-*-*-*"; +static const char *normbgcolor = "#222222"; +static const char *normfgcolor = "#cccccc"; +static const char *pressbgcolor = "#ffffff"; +static const char *pressfgcolor = "#555555"; + diff --git a/config.mk b/config.mk @@ -0,0 +1,26 @@ +# thingmenu metadata +NAME = thingmenu +VERSION = 0.2 + +# Customize below to fit your system + +# paths +PREFIX ?= /usr +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +# includes and libs +INCS = -I. -I/usr/include +LIBS = -L/usr/lib -L${X11LIB} -lc -lX11 + +# flags +CPPFLAGS = -DVERSION=\"${VERSION}\" +CFLAGS = -g -std=gnu99 -pedantic -Wall -O0 ${INCS} ${CPPFLAGS} +LDFLAGS = -g ${LIBS} +#LDFLAGS = -s ${LIBS} + +# compiler and linker +CC = cc + diff --git a/thingmenu.c b/thingmenu.c @@ -0,0 +1,555 @@ +/* + * Copy me if you can. + * by 20h + */ +#include <unistd.h> +#include <locale.h> +#include <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <libgen.h> +#include <X11/keysym.h> +#include <X11/Xatom.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include <X11/Xproto.h> +#include <X11/extensions/XTest.h> + +/* macros */ +#define MAX(a, b) ((a) > (b) ? (a) : (b)) +#define LENGTH(x) (sizeof x / sizeof x[0]) + +/* enums */ +enum { ColFG, ColBG, ColLast }; +enum { NetWMWindowType, NetLast }; + +/* typedefs */ +typedef unsigned int uint; +typedef unsigned long ulong; + +typedef struct { + ulong norm[ColLast]; + ulong press[ColLast]; + Drawable drawable; + GC gc; + struct { + int ascent; + int descent; + int height; + XFontSet set; + XFontStruct *xfont; + } font; +} DC; /* draw context */ + +typedef struct { + char *label; + char *cmd; + uint width; + int x, y, w, h; + Bool pressed; + Bool forceexit; +} Entry; + +/* function declarations */ +static void buttonpress(XEvent *e); +static void buttonrelease(XEvent *e); +static void cleanup(void); +static void configurenotify(XEvent *e); +static void unmapnotify(XEvent *e); +static void die(const char *errstr, ...); +static void drawmenu(void); +static void drawentry(Entry *e); +static void expose(XEvent *e); +static Entry *findentry(int x, int y); +static ulong getcolor(const char *colstr); +static void initfont(const char *fontstr); +static void leavenotify(XEvent *e); +static void press(Entry *e); +static void run(void); +static void setup(void); +static int textnw(const char *text, uint len); +static void unpress(void); +static void updateentries(void); + +/* variables */ +static int screen; +static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, + [ButtonRelease] = buttonrelease, + [ConfigureNotify] = configurenotify, + [UnmapNotify] = unmapnotify, + [Expose] = expose, + [LeaveNotify] = leavenotify, +}; + +static Display *dpy; +static DC dc; +static Window root, win; +static Bool running = True; +static int ww = 0, wh = 0, wx = 0, wy = 0; +static char *name = "thingmenu"; + +Entry **entries = NULL; +int nentries = 0; +int oneshot = 1; + +/* configuration, allows nested code to access above variables */ +#include "config.h" + +void +buttonpress(XEvent *e) +{ + XButtonPressedEvent *ev = &e->xbutton; + Entry *en; + + if((en = findentry(ev->x, ev->y))) + press(en); +} + +void +buttonrelease(XEvent *e) +{ + XButtonPressedEvent *ev = &e->xbutton; + Entry *en; + + if((en = findentry(ev->x, ev->y))) + unpress(); +} + +void +cleanup(void) +{ + if(dc.font.set) + XFreeFontSet(dpy, dc.font.set); + else + XFreeFont(dpy, dc.font.xfont); + XFreePixmap(dpy, dc.drawable); + XFreeGC(dpy, dc.gc); + XDestroyWindow(dpy, win); + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); +} + +void +configurenotify(XEvent *e) +{ + XConfigureEvent *ev = &e->xconfigure; + + if(ev->window == win && (ev->width != ww || ev->height != wh)) { + ww = ev->width; + wh = ev->height; + XFreePixmap(dpy, dc.drawable); + dc.drawable = XCreatePixmap(dpy, root, ww, wh, + DefaultDepth(dpy, screen)); + updateentries(); + } +} + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + exit(EXIT_FAILURE); +} + +void +drawmenu(void) +{ + int i; + + for(i = 0; i < nentries; i++) + drawentry(entries[i]); + XSync(dpy, False); +} + +void +drawentry(Entry *e) +{ + int x, y, h, len; + XRectangle r = { e->x, e->y, e->w, e->h}; + const char *l; + ulong *col; + + if(e->pressed) + col = dc.press; + else + col = dc.norm; + + XSetForeground(dpy, dc.gc, col[ColBG]); + XFillRectangles(dpy, dc.drawable, dc.gc, &r, 1); + XSetForeground(dpy, dc.gc, dc.norm[ColFG]); + r.height -= 1; + r.width -= 1; + XDrawRectangles(dpy, dc.drawable, dc.gc, &r, 1); + XSetForeground(dpy, dc.gc, col[ColFG]); + + l = e->label; + len = strlen(l); + h = dc.font.height; + y = e->y + (e->h / 2) - (h / 2) + dc.font.ascent; + x = e->x + (e->w / 2) - (textnw(l, len) / 2); + if(dc.font.set) { + XmbDrawString(dpy, dc.drawable, dc.font.set, dc.gc, x, y, l, + len); + } else + XDrawString(dpy, dc.drawable, dc.gc, x, y, l, len); + XCopyArea(dpy, dc.drawable, win, dc.gc, e->x, e->y, e->w, e->h, + e->x, e->y); +} + +void +unmapnotify(XEvent *e) +{ + running = False; +} + +void +expose(XEvent *e) +{ + XExposeEvent *ev = &e->xexpose; + + if(ev->count == 0 && (ev->window == win)) + drawmenu(); +} + +Entry * +findentry(int x, int y) +{ + int i; + + for(i = 0; i < nentries; i++) { + if(x > entries[i]->x && x < entries[i]->x + entries[i]->w + && y > entries[i]->y + && y < entries[i]->y + entries[i]->h) + return entries[i]; + } + return NULL; +} + +ulong +getcolor(const char *colstr) +{ + Colormap cmap = DefaultColormap(dpy, screen); + XColor color; + + if(!XAllocNamedColor(dpy, cmap, colstr, &color, &color)) + die("error, cannot allocate color '%s'\n", colstr); + return color.pixel; +} + +void +initfont(const char *fontstr) +{ + char *def, **missing; + int i, n; + + missing = NULL; + if(dc.font.set) + XFreeFontSet(dpy, dc.font.set); + dc.font.set = XCreateFontSet(dpy, fontstr, &missing, &n, &def); + if(missing) { + while(n--) + fprintf(stderr, "svkbd: missing fontset: %s\n", missing[n]); + XFreeStringList(missing); + } + if(dc.font.set) { + XFontSetExtents *font_extents; + XFontStruct **xfonts; + char **font_names; + dc.font.ascent = dc.font.descent = 0; + font_extents = XExtentsOfFontSet(dc.font.set); + n = XFontsOfFontSet(dc.font.set, &xfonts, &font_names); + for(i = 0, dc.font.ascent = 0, dc.font.descent = 0; i < n; i++) { + dc.font.ascent = MAX(dc.font.ascent, (*xfonts)->ascent); + dc.font.descent = MAX(dc.font.descent,(*xfonts)->descent); + xfonts++; + } + } + else { + if(dc.font.xfont) + XFreeFont(dpy, dc.font.xfont); + dc.font.xfont = NULL; + if(!(dc.font.xfont = XLoadQueryFont(dpy, fontstr)) + && !(dc.font.xfont = XLoadQueryFont(dpy, "fixed"))) + die("error, cannot load font: '%s'\n", fontstr); + dc.font.ascent = dc.font.xfont->ascent; + dc.font.descent = dc.font.xfont->descent; + } + dc.font.height = dc.font.ascent + dc.font.descent; +} + +void +leavenotify(XEvent *e) +{ + unpress(); +} + +void +runentry(Entry *e) +{ + char *shell; + + if (fork()) { + if (oneshot || e->forceexit) { + XDestroyWindow(dpy, win); + exit(0); + } + return; + } + if (fork()) + exit(0); + + shell = getenv("SHELL"); + if (!shell) + shell = "/bin/sh"; + + execlp(shell, basename(shell), "-c", e->cmd, (char *)NULL); +} + +void +press(Entry *e) +{ + e->pressed = !e->pressed; + + runentry(e); + drawentry(e); +} + +void +run(void) +{ + XEvent ev; + + /* main event loop */ + XSync(dpy, False); + while(running) { + XNextEvent(dpy, &ev); + if(handler[ev.type]) + (handler[ev.type])(&ev); /* call handler */ + } +} + +void +setup(void) +{ + XSetWindowAttributes wa; + XTextProperty str; + XSizeHints *sizeh; + XClassHint *ch; + int i, sh, sw, ls; + + /* init screen */ + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); + sw = DisplayWidth(dpy, screen) - 1; + sh = DisplayHeight(dpy, screen) - 1; + initfont(font); + + /* init atoms */ + + /* init appearance */ + + if (ww == 0) { + for (i = 0, ww = 0; i < nentries; i++) { + ls = textnw(entries[i]->label, + strlen(entries[i]->label)); + if (ls > ww) + ww = ls; + } + ww *= 1.5; + } + if (wh == 0) + wh = (nentries + 2) * dc.font.height + 4; + if (wy == 0) + wy = (sh - wh) / 2; + if (wx == 0) + wx = (sw - ww) / 2; + + dc.norm[ColBG] = getcolor(normbgcolor); + dc.norm[ColFG] = getcolor(normfgcolor); + dc.press[ColBG] = getcolor(pressbgcolor); + dc.press[ColFG] = getcolor(pressfgcolor); + dc.drawable = XCreatePixmap(dpy, root, ww, wh, DefaultDepth(dpy, screen)); + dc.gc = XCreateGC(dpy, root, 0, 0); + if(!dc.font.set) + XSetFont(dpy, dc.gc, dc.font.xfont->fid); + for(i = 0; i < nentries; i++) + entries[i]->pressed = 0; + + wa.override_redirect = !wmborder; + wa.border_pixel = dc.norm[ColFG]; + wa.background_pixel = dc.norm[ColBG]; + win = XCreateWindow(dpy, root, wx, wy, ww, wh, 0, + CopyFromParent, CopyFromParent, CopyFromParent, + CWOverrideRedirect | CWBorderPixel | CWBackingPixel, &wa); + XSelectInput(dpy, win, StructureNotifyMask|ButtonReleaseMask| + ButtonPressMask|ExposureMask|LeaveWindowMask); + + sizeh = XAllocSizeHints(); + sizeh->flags = PMaxSize | PMinSize; + sizeh->min_width = sizeh->max_width = ww; + sizeh->min_height = sizeh->max_height = wh; + XStringListToTextProperty(&name, 1, &str); + ch = XAllocClassHint(); + ch->res_class = name; + ch->res_name = name; + + XSetWMProperties(dpy, win, &str, &str, NULL, 0, sizeh, NULL, + ch); + + XFree(ch); + XFree(str.value); + XFree(sizeh); + + XMapRaised(dpy, win); + updateentries(); + drawmenu(); +} + +int +textnw(const char *text, uint len) +{ + XRectangle r; + + if(dc.font.set) { + XmbTextExtents(dc.font.set, text, len, NULL, &r); + return r.width; + } + return XTextWidth(dc.font.xfont, text, len); +} + +void +unpress() +{ + int i; + + for(i = 0; i < nentries; i++) { + if(entries[i]->pressed) { + entries[i]->pressed = 0; + drawentry(entries[i]); + } + } +} + +void +updateentries(void) +{ + int i, y = 0, h; + + h = wh / nentries; + for(i = 0; i < nentries; i++) { + entries[i]->x = 0; + entries[i]->y = y; + entries[i]->w = ww; + entries[i]->h = h; + y += h; + } +} + +void +usage(char *argv0) +{ + fprintf(stderr, "usage: %s [-hs] [-wh height] [-ww width] " + "[-wx x position] [-wy y position] [--] " + "label:cmd ...\n", argv0); + exit(1); +} + +int +main(int argc, char *argv[]) +{ + char *label, *cmd; + int i; + + if (argc < 2) + usage(argv[0]); + i = 1; + for (; argv[i]; i++) { + if (argv[i][0] != '-') + break; + + if (argv[i][1] == '-') { + i++; + break; + } + + switch (argv[i][1]) { + case 'h': + usage(argv[0]); + case 's': + oneshot = 0; + break; + case 'w': + switch ((i >= argc - 1)? 0 : argv[i][2]) { + case 'h': + wh = atoi(argv[i+1]); + i++; + break; + case 'w': + ww = atoi(argv[i+1]); + i++; + break; + case 'x': + wx = atoi(argv[i+1]); + i++; + break; + case 'y': + wy = atoi(argv[i+1]); + i++; + break; + default: + usage(argv[0]); + } + break; + default: + usage(argv[0]); + } + } + + for (; argv[i]; i++) { + sscanf(argv[i], "%1024m[^:]:%1024m[^\n]", &label, &cmd); + if (label == NULL || cmd == NULL) { + if (label == NULL) + free(label); + if (cmd == NULL) + free(cmd); + usage(argv[0]); + } + entries = realloc(entries, sizeof(entries[0])*(++nentries)); + entries[nentries-1] = malloc(sizeof(*entries[0])); + bzero(entries[nentries-1], sizeof(*entries[0])); + entries[nentries-1]->label = label; + entries[nentries-1]->cmd = cmd; + } + if (nentries < 1) + usage(argv[0]); + + entries = realloc(entries, sizeof(entries[0])*(++nentries)); + entries[nentries-1] = malloc(sizeof(*entries[0])); + bzero(entries[nentries-1], sizeof(*entries[0])); + entries[nentries-1]->label = strdup("cancel"); + entries[nentries-1]->cmd = "exit"; + entries[nentries-1]->forceexit = True; + + if(!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fprintf(stderr, "warning: no locale support\n"); + if(!(dpy = XOpenDisplay(0))) + die("thingmenu: cannot open display\n"); + + setup(); + run(); + cleanup(); + XCloseDisplay(dpy); + + for (i = 0; i < nentries; i++) + free(entries[i]); + free(entries); + + return 0; +} +