wmii/cmd/wmimenu.c

622 lines
15 KiB
C

/*
* (C)opyright MMIV-MMV Anselm R. Garbe <garbeam at gmail dot com>
* See LICENSE file for license details.
*/
#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#include <X11/Xproto.h>
#include <X11/Xutil.h>
#include <X11/keysym.h>
#include "wmii.h"
/* array indexes for file pointers */
typedef enum {
M_CTL,
M_GEOMETRY,
M_PRE_COMMAND,
M_COMMAND,
M_HISTORY,
M_LOOKUP,
M_FONT,
M_SEL_BG_COLOR,
M_SEL_TEXT_COLOR,
M_SEL_BORDER_COLOR,
M_NORM_BG_COLOR,
M_NORM_TEXT_COLOR,
M_NORM_BORDER_COLOR,
M_LAST
} InputIndexes;
enum {
OFF_NEXT, OFF_PREV, OFF_CURR, OFF_LAST
};
static IXPServer *ixps = 0;
static Display *dpy;
static GC gc;
static Window win;
static XRectangle rect;
static XRectangle mrect;
static int screen_num;
static char *sockfile = 0;
static File *files[M_LAST];
static Container items = {0};
static Container history = {0};
static int offset[OFF_LAST];
static unsigned int cmdw = 0;
static Pixmap pmap;
static const int seek = 30; /* 30px */
static XFontStruct *font;
static unsigned int sel = 0;
static Align align = SOUTH;
static void check_event(Connection * c);
static void draw_menu(void);
static void handle_kpress(XKeyEvent * e);
static void set_text(char *text);
static void quit(void *obj, char *arg);
static void display(void *obj, char *arg);
static int update_items(char *prefix);
static Action acttbl[2] = {
{"quit", quit},
{"display", display}
};
static char *version[] = {
"wmimenu - window manager improved menu - " VERSION "\n"
" (C)opyright MMIV-MMV Anselm R. Garbe\n", 0
};
static void usage()
{
fprintf(stderr, "%s",
"usage: wmimenu [-s <socket file>] [-r] [-v]\n"
" -s socket file (default: /tmp/.ixp-$USER/wmimenu-%s-%s)\n"
" -v version info\n");
exit(1);
}
static void add_history(char *cmd)
{
char buf[MAX_BUF];
snprintf(buf, MAX_BUF, "/history/%ld", (long) time(0));
cext_attach_item(&history, wmii_create_ixpfile(ixps, buf, cmd));
}
static void exec_item(char *cmd)
{
File *item = cext_list_get_item(&items, sel);
char *rc = cmd;
if (!cmd || cmd[0] == 0)
return;
if (item && item->size)
rc = cmd = item->content;
add_history(cmd);
if (files[M_PRE_COMMAND]->content) {
size_t len = strlen(cmd) + files[M_PRE_COMMAND]->size + 2;
rc = cext_emallocz(len);
snprintf(rc, len, "%s %s", (char *) files[M_PRE_COMMAND]->content, cmd);
}
/* fallback */
spawn(dpy, rc);
/* cleanup */
if (files[M_PRE_COMMAND]->content)
free(rc);
}
static void quit(void *obj, char *arg)
{
ixps->runlevel = SHUTDOWN;
}
static void show()
{
set_text(0);
XMapRaised(dpy, win);
XSync(dpy, False);
update_items(files[M_COMMAND]->content);
draw_menu();
while (XGrabKeyboard
(dpy, RootWindow(dpy, screen_num), True, GrabModeAsync,
GrabModeAsync, CurrentTime) != GrabSuccess)
usleep(1000);
}
static void hide()
{
XUngrabKeyboard(dpy, CurrentTime);
XUnmapWindow(dpy, win);
XSync(dpy, False);
}
static void display(void *obj, char *arg)
{
if (!arg)
return;
if (_strtonum(arg, 0, 1))
show();
else
hide();
check_event(0);
}
void set_text(char *text)
{
if (files[M_COMMAND]->content) {
free(files[M_COMMAND]->content);
files[M_COMMAND]->content = 0;
files[M_COMMAND]->size = 0;
}
if (text && strlen(text)) {
files[M_COMMAND]->content = strdup(text);
files[M_COMMAND]->size = strlen(text);
}
}
static void update_offsets()
{
File *item;
unsigned int i, w = cmdw + 2 * seek;
if (!cext_stack_get_top_item(&items))
return;
/* calc next offset */
for (i = offset[OFF_CURR]; (item = cext_list_get_item(&items, i)); i++) {
w += XTextWidth(font, item->content, strlen(item->content)) + mrect.height;
if (w > mrect.width)
break;
}
offset[OFF_NEXT] = i;
w = cmdw + 2 * seek;
for (i = offset[OFF_CURR] - 1; (i >= 0) && (item = cext_list_get_item(&items, i)); i--) {
w += XTextWidth(font, item->content, strlen(item->content)) + mrect.height;
if (w > mrect.width)
break;
}
offset[OFF_PREV] = i + 1;
}
static int update_items(char *pattern)
{
size_t plen = pattern ? strlen(pattern) : 0, len, max = 0, size;
int matched = pattern ? plen == 0 : 1;
File *f, *p, *maxitem;
if (!files[M_LOOKUP]->content)
return 0;
f = ixp_walk(ixps, files[M_LOOKUP]->content);
if (!f || !is_directory(f))
return 0;
cmdw = 0;
offset[OFF_CURR] = offset[OFF_PREV] = offset[OFF_NEXT] = 0;
sel = 0;
while ((p = cext_stack_get_top_item(&items)))
cext_detach_item(&items, p);
/* build new items */
for (p = f->content; p; p = p->next) {
len = strlen(p->name);
if (max < len) {
maxitem = p;
max = len;
}
}
if (maxitem)
cmdw = XTextWidth(font, maxitem->name, max) + mrect.height;
for (p = f->content; p; p = p->next) {
if (matched || !strncmp(pattern, p->name, plen)) {
cext_attach_item(&items, p);
p->parent = 0; /* HACK to prevent doubled items */
}
}
for (p = f->content; p; p = p->next) {
if (p->parent && strstr(p->name, pattern))
cext_attach_item(&items, p);
else
p->parent = f; /* restore HACK */
}
size = cext_sizeof(&items);
update_offsets();
return size;
}
/* creates draw structs for menu mode drawing */
static void draw_menu()
{
Draw d = { 0 };
unsigned int offx = 0;
int i = 0;
File *item = 0, *selitem = cext_list_get_item(&items, sel);
d.gc = gc;
d.font = font;
d.drawable = pmap;
d.rect = mrect;
d.rect.x = 0;
d.rect.y = 0;
d.bg = blitz_loadcolor(dpy, screen_num, files[M_NORM_BG_COLOR]->content);
d.border = blitz_loadcolor(dpy, screen_num, files[M_NORM_BORDER_COLOR]->content);
blitz_drawlabelnoborder(dpy, &d);
/* print command */
d.align = WEST;
d.font = font;
d.fg = blitz_loadcolor(dpy, screen_num, files[M_NORM_TEXT_COLOR]->content);
d.data = files[M_COMMAND]->content;
if (cmdw && selitem)
d.rect.width = cmdw;
offx += d.rect.width;
blitz_drawlabelnoborder(dpy, &d);
d.align = CENTER;
if (selitem) {
d.bg = blitz_loadcolor(dpy, screen_num, files[M_NORM_BG_COLOR]->content);
d.fg = blitz_loadcolor(dpy, screen_num, files[M_NORM_TEXT_COLOR]->content);
d.data = offset[OFF_CURR] ? "<" : 0;
d.rect.x = offx;
d.rect.width = seek;
offx += d.rect.width;
blitz_drawlabelnoborder(dpy, &d);
/* determine maximum items */
for (i = offset[OFF_CURR]; (i < offset[OFF_NEXT]) && (item = cext_list_get_item(&items, i)); i++) {
d.data = item->name;
d.rect.x = offx;
d.rect.width = XTextWidth(d.font, d.data, strlen(d.data)) + mrect.height;
offx += d.rect.width;
/*fprintf(stderr, "%s (%d, %d, %d, %d)\n", item->name, d.rect.x, d.rect.y, d.rect.width, d.rect.height);*/
if (selitem == item) {
d.bg = blitz_loadcolor(dpy, screen_num, files[M_SEL_BG_COLOR]->content);
d.fg = blitz_loadcolor(dpy, screen_num, files[M_SEL_TEXT_COLOR]->content);
d.border = blitz_loadcolor(dpy, screen_num, files[M_SEL_BORDER_COLOR]-> content);
blitz_drawlabel(dpy, &d);
} else {
d.bg = blitz_loadcolor(dpy, screen_num, files[M_NORM_BG_COLOR]->content);
d.fg = blitz_loadcolor(dpy, screen_num, files[M_NORM_TEXT_COLOR]->content);
d.border = blitz_loadcolor(dpy, screen_num, files[M_NORM_BORDER_COLOR]->content);
blitz_drawlabelnoborder(dpy, &d);
}
}
d.bg = blitz_loadcolor(dpy, screen_num, files[M_NORM_BG_COLOR]->content);
d.fg = blitz_loadcolor(dpy, screen_num, files[M_NORM_TEXT_COLOR]->content);
d.data = item ? ">" : 0;
d.rect.x = mrect.width - seek;
d.rect.width = seek;
blitz_drawlabelnoborder(dpy, &d);
}
XCopyArea(dpy, pmap, win, gc, 0, 0, mrect.width, mrect.height, 0, 0);
XSync(dpy, False);
}
static void handle_kpress(XKeyEvent * e)
{
KeySym ksym;
char buf[32];
int num;
static char text[4096];
size_t len = 0, size = cext_sizeof(&items);
File *selitem = cext_list_get_item(&items, sel);
File *hist = cext_stack_get_top_item(&history);
text[0] = 0;
if (files[M_COMMAND]->content) {
cext_strlcpy(text, files[M_COMMAND]->content, sizeof(text));
len = strlen(text);
}
buf[0] = 0;
num = XLookupString(e, buf, sizeof(buf), &ksym, 0);
if (IsFunctionKey(ksym) || IsKeypadKey(ksym)
|| IsMiscFunctionKey(ksym) || IsPFKey(ksym)
|| IsPrivateKeypadKey(ksym))
return;
/* first check if a control mask is omitted */
if (e->state & ShiftMask) {
if (ksym == XK_ISO_Left_Tab)
ksym = XK_Left;
} else if (e->state & ControlMask) {
switch (ksym) {
case XK_E:
case XK_e:
ksym = XK_End;
break;
case XK_H:
case XK_h:
ksym = XK_BackSpace;
break;
case XK_J:
case XK_j:
ksym = XK_Return;
break;
case XK_U:
case XK_u:
set_text(0);
update_items(0);
draw_menu();
return;
break;
default: /* ignore other control sequences */
return;
break;
}
}
switch (ksym) {
case XK_Left:
if (!selitem)
return;
if (sel > 0) {
selitem = cext_list_get_item(&items, --sel);
set_text(selitem->name);
} else
return;
break;
case XK_Right:
case XK_Tab:
if (!selitem)
return;
if (sel < size - 1) {
selitem = cext_list_get_item(&items, ++sel);
set_text(selitem->name);
} else
return;
break;
case XK_Down:
if (hist) {
set_text(hist->content);
cext_stack_top_item(&history, cext_list_get_next_item(&items, hist));
}
update_items(files[M_COMMAND]->content);
break;
case XK_Up:
if (hist) {
set_text(hist->content);
cext_stack_top_item(&history, cext_list_get_prev_item(&items, hist));
}
update_items(files[M_COMMAND]->content);
break;
case XK_Return:
if (selitem)
exec_item(selitem->name);
else if (text)
exec_item(text);
case XK_Escape:
hide();
break;
case XK_BackSpace:
if (len) {
size_t i = len;
if (i) {
do
text[--i] = 0;
while (size && i && size == update_items(text));
}
set_text(text);
update_items(files[M_COMMAND]->content);
}
break;
default:
if ((num == 1) && !iscntrl((int) buf[0])) {
buf[num] = 0;
if (len > 0)
cext_strlcat(text, buf, sizeof(text));
else
cext_strlcpy(text, buf, sizeof(text));
set_text(text);
update_items(files[M_COMMAND]->content);
}
}
if (selitem) {
if (sel < offset[OFF_CURR]) {
offset[OFF_CURR] = offset[OFF_PREV];
update_offsets();
} else if (sel >= offset[OFF_NEXT]) {
offset[OFF_CURR] = offset[OFF_NEXT];
update_offsets();
}
}
draw_menu();
}
static void check_event(Connection * c)
{
XEvent ev;
while (XPending(dpy)) {
XNextEvent(dpy, &ev);
switch (ev.type) {
case KeyPress:
handle_kpress(&ev.xkey);
break;
case Expose:
if (ev.xexpose.count == 0) {
draw_menu();
}
break;
default:
break;
}
}
}
static void update_geometry()
{
mrect = rect;
mrect.height = font->ascent + font->descent + 4;
if (align == SOUTH)
mrect.y = rect.height - mrect.height;
XMoveResizeWindow(dpy, win, mrect.x, mrect.y, mrect.width, mrect.height);
XSync(dpy, False);
XFreePixmap(dpy, pmap);
pmap = XCreatePixmap(dpy, win, mrect.width, mrect.height, DefaultDepth(dpy, screen_num));
XSync(dpy, False);
}
static void handle_after_write(IXPServer * s, File * f)
{
int i;
size_t len;
if (files[M_CTL] == f) {
for (i = 0; i < 2; i++) {
len = strlen(acttbl[i].name);
if (!strncmp(acttbl[i].name, (char *) f->content, len)) {
if (strlen(f->content) > len) {
acttbl[i].func(0, &((char *) f->content)[len + 1]);
} else {
acttbl[i].func(0, 0);
}
break;
}
}
} else if (files[M_GEOMETRY] == f) {
if (f->content) {
if (!strncmp(f->content, "south", 6))
align = SOUTH;
else if(!strncmp(f->content, "north", 6))
align = NORTH;
update_geometry();
draw_menu();
}
} else if (files[M_FONT] == f) {
XFreeFont(dpy, font);
font = blitz_getfont(dpy, files[M_FONT]->content);
update_geometry();
draw_menu();
} else if (files[M_COMMAND] == f) {
update_items(files[M_COMMAND]->content);
draw_menu();
}
check_event(0);
}
static void handle_before_read(IXPServer * s, File * f)
{
char buf[64];
if (f != files[M_GEOMETRY])
return;
snprintf(buf, sizeof(buf), "%d,%d,%d,%d", mrect.x, mrect.y,
mrect.width, mrect.height);
if (f->content)
free(f->content);
f->content = strdup(buf);
f->size = strlen(buf);
}
int main(int argc, char *argv[])
{
int i;
XSetWindowAttributes wa;
XGCValues gcv;
/* command line args */
for (i = 1; (i < argc) && (argv[i][0] == '-'); i++) {
switch (argv[i][1]) {
case 'v':
fprintf(stdout, "%s", version[0]);
exit(0);
break;
case 's':
if (i + 1 < argc)
sockfile = argv[++i];
else
usage();
break;
default:
usage();
break;
}
}
dpy = XOpenDisplay(0);
if (!dpy) {
fprintf(stderr, "%s", "wmimenu: cannot open display\n");
exit(1);
}
screen_num = DefaultScreen(dpy);
ixps = wmii_setup_server(sockfile);
/* init */
if (!(files[M_CTL] = ixp_create(ixps, "/ctl"))) {
perror("wmimenu: cannot connect IXP server");
exit(1);
}
files[M_CTL]->after_write = handle_after_write;
files[M_GEOMETRY] = ixp_create(ixps, "/geometry");
files[M_GEOMETRY]->before_read = handle_before_read;
files[M_GEOMETRY]->after_write = handle_after_write;
files[M_PRE_COMMAND] = ixp_create(ixps, "/precmd");
files[M_COMMAND] = ixp_create(ixps, "/cmd");
files[M_COMMAND]->after_write = handle_after_write;
files[M_HISTORY] = ixp_create(ixps, "/history");
add_history("");
files[M_LOOKUP] = ixp_create(ixps, "/lookup");
files[M_FONT] = wmii_create_ixpfile(ixps, "/font", BLITZ_FONT);
files[M_FONT]->after_write = handle_after_write;
font = blitz_getfont(dpy, files[M_FONT]->content);
files[M_SEL_BG_COLOR] = wmii_create_ixpfile(ixps, "/sstyle/bgcolor", BLITZ_SEL_BG_COLOR);
files[M_SEL_TEXT_COLOR] = wmii_create_ixpfile(ixps, "/sstyle/fgcolor", BLITZ_SEL_FG_COLOR);
files[M_SEL_BORDER_COLOR] = wmii_create_ixpfile(ixps, "/sstyle/bordercolor", BLITZ_SEL_BORDER_COLOR);
files[M_NORM_BG_COLOR] = wmii_create_ixpfile(ixps, "/nstyle/bgcolor", BLITZ_NORM_BG_COLOR);
files[M_NORM_TEXT_COLOR] = wmii_create_ixpfile(ixps, "/nstyle/fgcolor", BLITZ_NORM_FG_COLOR);
files[M_NORM_BORDER_COLOR] = wmii_create_ixpfile(ixps, "/nstyle/bordercolor", BLITZ_NORM_BORDER_COLOR);
wa.override_redirect = 1;
wa.background_pixmap = ParentRelative;
wa.event_mask = ExposureMask | ButtonPressMask | KeyPressMask
| SubstructureRedirectMask | SubstructureNotifyMask;
rect.x = rect.y = 0;
rect.width = DisplayWidth(dpy, screen_num);
rect.height = DisplayHeight(dpy, screen_num);
mrect = rect;
mrect.height = font->ascent + font->descent + 4;
mrect.y = rect.height - mrect.height;
win = XCreateWindow(dpy, RootWindow(dpy, screen_num), mrect.x, mrect.y,
mrect.width, mrect.height, 0, DefaultDepth(dpy, screen_num),
CopyFromParent, DefaultVisual(dpy, screen_num),
CWOverrideRedirect | CWBackPixmap | CWEventMask, &wa);
XDefineCursor(dpy, win, XCreateFontCursor(dpy, XC_xterm));
XSync(dpy, False);
/* window pixmap */
gcv.function = GXcopy;
gcv.graphics_exposures = False;
gc = XCreateGC(dpy, win, 0, 0);
pmap = XCreatePixmap(dpy, win, mrect.width, mrect.height, DefaultDepth(dpy, screen_num));
/* main event loop */
run_server_with_fd_support(ixps, ConnectionNumber(dpy), check_event, 0);
deinit_server(ixps);
XFreePixmap(dpy, pmap);
XFreeGC(dpy, gc);
XCloseDisplay(dpy);
return 0;
}