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;
+}
+