This repository has been archived on 2020-12-21. You can view files and clone it, but cannot push or open issues or pull requests.
nenu/nenu.c

633 lines
13 KiB
C
Raw Normal View History

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <X11/Xlib.h>
#include <X11/keysym.h>
#include <X11/Xutil.h>
#include <X11/Xft/Xft.h>
#include <X11/extensions/Xrender.h>
2015-04-04 09:25:57 +00:00
#include <X11/extensions/Xinerama.h>
#include <fontconfig/fontconfig.h>
#include <ctype.h>
#include <strings.h>
#define MAX(A, B) (A > B ? A : B)
#include "nenu.h"
#include "config.h"
Window root, win;
int screen;
Display *display;
GC gc;
Drawable buf;
XftDraw *draw;
Pixmap nullpixmap;
XIM xim;
XIC xic;
XftColor fg, bg;
XftFont *font;
int ascent, descent;
2015-04-04 09:25:57 +00:00
int heading_width, cursor_width;
2015-04-04 22:15:44 +00:00
int x = -1, y = -1;
int w = 0, h = 0;
int exit_on_one = 0;
int complete_on_exit = 0;
int input_bar = 1;
int read_options = 1;
int absolute_position = 0;
size_t cursor;
2015-04-08 04:01:50 +00:00
FcChar8 *text;
FcChar8 *heading;
option *options;
option *current, *valid;
void usage() {
printf(
2015-04-04 22:15:44 +00:00
"usage: nenu [-a|-c|-n|-e|-p x,y|-ap] [header]\n\n"
"nenu takes options from stdin and display them, allowing the user to input \n"
"their desired option\n\n"
" -a : exits as soon as there is only one match.\n"
" -c : on exit return the match at the begining of the list. if there is\n"
" no match nenu returns the user input\n"
" -n : no input bar is displayed and -c is put on.\n"
" -e : takes no input from stdin\n"
"\n"
" -p x,y: set window position\n"
" -ap : don't shift the window so it stays inside the screen\n"
"\n"
" -fg #ff00ff : set foreground and border color\n"
" -bg #000000 : set background color\n"
" -fn font : set font.\n"
);
}
2015-04-08 04:01:50 +00:00
void die(FcChar8 *msg) {
fprintf(stderr, "nenu [ERROR]: %s\n", msg);
exit(EXIT_FAILURE);
}
void finish() {
if (complete_on_exit) {
if (current) printf("%s\n", current->text);
else if (valid) printf("%s\n", valid->text);
} else if (exit_on_one) {
if (valid) printf("%s\n", valid->text);
} else {
printf("%s\n", text);
}
2015-04-04 22:15:44 +00:00
exit(0);
}
2015-04-08 04:01:50 +00:00
int text_width(FcChar8 *str) {
XGlyphInfo g;
int len = strlen(str);
XftTextExtentsUtf8(display, font, str, len, &g);
return g.width;
}
2015-04-08 04:01:50 +00:00
void draw_string(FcChar8 *str, int x, int y) {
XftDrawStringUtf8(draw, &fg, font, x, y, (FcChar8 *) str, strlen(str));
}
void render_options(int oy) {
option *o;
int flipped;
if (current)
o = current;
else
o = valid;
for (flipped = 0; o; o = !o->next && !(flipped++) ? valid : o->next) {
if (flipped && o == current)
break;
draw_string(o->text, PADDING, oy + ascent);
oy += ascent + descent;
}
}
void render() {
2015-04-08 04:01:50 +00:00
FcChar8 *cursor_text;
int cursor_pos;
update_size();
XftDrawRect(draw, &bg, 0, 0, w, h);
if (input_bar) {
2015-04-08 04:01:50 +00:00
cursor_text = malloc(sizeof(FcChar8) * (cursor + 1));
strncpy(cursor_text, text, cursor);
cursor_text[cursor] = '\0';
cursor_pos = text_width(cursor_text);
free(cursor_text);
2015-04-04 09:25:57 +00:00
draw_string(heading, PADDING, PADDING + ascent);
draw_string(text, PADDING + heading_width, PADDING + ascent);
draw_string("_", PADDING + heading_width + cursor_pos,
PADDING + ascent);
render_options(PADDING + ascent + 5);
} else
render_options(PADDING);
XCopyArea(display, buf, win, gc, 0, 0, w, h, 0, 0);
}
2015-04-08 04:01:50 +00:00
void insert(FcChar8 *str, ssize_t n) {
int i, j;
2015-04-04 22:15:44 +00:00
if (strlen(text) + n > MAX_LEN)
return;
2015-04-04 22:15:44 +00:00
memmove(&text[cursor + n], &text[cursor], MAX_LEN - cursor - MAX(n, 0));
if (n > 0)
memcpy(&text[cursor], str, n);
cursor += n;
}
// return location of next utf8 rune in the given direction -- thanks dmenu
size_t nextrune(int inc) {
ssize_t n;
for (n = cursor + inc; n + inc >= 0 && (text[n] & 0xc0) == 0x80; n += inc);
return n;
}
void handle_button(XButtonEvent be) {
option *o;
switch(be.button) {
case Button4:
if (current && current->prev)
current = current->prev;
else {
for (o = valid; o && o->next; o = o->next);
current = o;
}
break;
case Button5:
if (current && current->next)
current = current->next;
else
current = valid;
break;
2015-04-04 22:15:44 +00:00
case Button1:
finish();
break;
case Button3:
finish();
break;
case Button2:
finish();
break;
}
render();
}
void handle_key(XKeyEvent ke) {
2015-04-08 04:01:50 +00:00
FcChar8 buf[32];
int len, n;
KeySym keysym = NoSymbol;
Status status;
option *o;
len = XmbLookupString(xic, &ke, buf, sizeof(buf), &keysym, &status);
if (status == XBufferOverflow)
return;
if (ke.state & ControlMask) {
switch(keysym) {
// Emacs
2015-04-04 22:15:44 +00:00
case XK_a:
keysym = XK_Home;
break;
case XK_e:
keysym = XK_End;
break;
case XK_p:
keysym = XK_Up;
break;
case XK_n:
keysym = XK_Down;
break;
case XK_f:
keysym = XK_Right;
break;
case XK_b:
keysym = XK_Left;
break;
case XK_d:
keysym = XK_Delete;
break;
// Vim
case XK_j:
keysym = XK_Down;
break;
case XK_k:
keysym = XK_Up;
break;
case XK_h:
keysym = XK_Left;
break;
case XK_l:
keysym = XK_Right;
break;
}
} else if (ke.state & Mod1Mask) {
}
2015-04-04 22:15:44 +00:00
if (input_bar) {
switch(keysym) {
default:
2015-04-08 04:01:50 +00:00
if (!iscntrl(*buf)) {
insert(buf, len);
2015-04-08 04:01:50 +00:00
update_valid_options();
}
2015-04-04 22:15:44 +00:00
break;
case XK_Delete: // How the fuck does this work?
if (text[cursor] == '\0')
return;
cursor = nextrune(+1);
update_valid_options();
case XK_BackSpace:
if (cursor == 0)
exit(0);
insert(NULL, nextrune(-1) - cursor);
update_valid_options();
2015-04-04 22:15:44 +00:00
break;
2015-04-04 22:15:44 +00:00
case XK_Tab:
select_forward_match();
update_valid_options();
break;
case XK_Left:
if (cursor != 0)
cursor = nextrune(-1);
break;
case XK_Right:
if (text[cursor] != '\0' && cursor < MAX_LEN - 1)
cursor = nextrune(+1);
break;
case XK_Home:
cursor = 0;
break;
case XK_End:
while (text[cursor] != '\0' && cursor < MAX_LEN - 1)
cursor = nextrune(+1);
}
}
switch(keysym) {
case XK_Up:
if (current->prev) {
current = current->prev;
} else {
for (o = valid; o && o->next; o = o->next);
current = o;
}
break;
case XK_Down:
if (current->next)
current = current->next;
else
current = valid;
break;
case XK_Return:
finish();
break;
case XK_Escape:
2015-04-04 22:15:44 +00:00
exit(1);
break;
}
if (exit_on_one) {
for (n = 0, o = valid; o; o = o->next) n++;
if (n == 1) {
current = valid;
finish();
}
}
render();
}
void update_valid_options() {
option *head = malloc(sizeof(option));
head->next = NULL;
option *v = head;
option *o;
for (o = options; o; o = o->next) {
if (strncmp(text, o->text, strlen(text)) == 0) {
v->next = malloc(sizeof(option));
v->next->prev = v;
v = v->next;
v->text = o->text;
v->next = NULL;
}
}
valid = head->next;
if (valid)
valid->prev = NULL;
free(head);
current = valid;
}
void select_forward_match() {
option *o;
int i, can_move = -1, good = 1;
2015-04-08 04:01:50 +00:00
FcChar8 c;
if (!valid)
return;
while (good) {
c = valid->text[++can_move];
if (!c) break;
for (o = valid; o && good; o = o->next)
if (o->text[can_move] != c)
good = 0;
}
if (can_move > 0)
for (i = 0; i < can_move; i++)
text[i] = valid->text[i];
cursor = can_move;
}
2015-04-08 04:01:50 +00:00
void load_font(FcChar8 *fontstr) {
FcPattern *pattern, *match;
FcResult result;
if (fontstr[0] == '-')
pattern = XftXlfdParse(fontstr, False, False);
else
pattern = FcNameParse((FcChar8 *) fontstr);
if (!pattern)
die("Failed to get font pattern");
match = FcFontMatch(NULL, pattern, &result);
if (!match)
die("Failed to get font match");
if (!(font = XftFontOpenPattern(display, match))) {
FcPatternDestroy(match);
die("Failed to open font");
}
ascent = font->ascent;
descent = font->descent;
}
void grab_keyboard_pointer() {
int i, p = 1, k = 1;
XColor dummycolor;
Cursor nullcursor;
nullpixmap = XCreatePixmap(display, root, 1, 1, 1);
nullcursor = XCreatePixmapCursor(display, nullpixmap, nullpixmap,
&dummycolor, &dummycolor, 0, 0);
for (i = 0; (k || p) && i < 1000; i++) {
2015-04-04 22:15:44 +00:00
if (p && XGrabPointer(display, root, False, ButtonPressMask,
GrabModeAsync, GrabModeAsync,
win, nullcursor, CurrentTime)
== GrabSuccess) p = 0;
if (k && XGrabKeyboard(display, root, True,
GrabModeAsync, GrabModeAsync,
CurrentTime) == GrabSuccess) k = 0;
usleep(1000);
}
if (p) die("Failed to grab pointer!");
if (k) die("Failed to grab keyboard!");
}
void update_size() {
int tw;
int ow = w, oh = h;
option *o;
2015-04-04 09:25:57 +00:00
w = heading_width + cursor_width + text_width(text);
h = input_bar ? ascent + descent : 0;
for (o = valid; o; o = o->next) {
tw = text_width(o->text);
if (tw > w)
w = tw;
h += ascent + descent;
}
w += PADDING * 2;
h += PADDING * 2;
if (ow == w && oh == h)
return;
XResizeWindow(display, win, w, h);
if (buf)
XFreePixmap(display, buf);
buf = XCreatePixmap(display, win, w, h, DefaultDepth(display, screen));
XftDrawChange(draw, buf);
}
void set_position() {
Window ww;
int c, v;
/* if x,y not set then set to cursor position. */
if (x == -1 && y == -1)
XQueryPointer(display, root, &ww, &ww, &x, &y, &c, &c, &v);
XMoveWindow(display, win, x, y);
}
2015-04-04 09:25:57 +00:00
void keep_in_screen() {
XineramaScreenInfo *info;
int i, count;
if ((info = XineramaQueryScreens(display, &count))) {
for (i = 0; i < count; i++)
2015-04-08 04:01:50 +00:00
if (info[i].x_org <= x
&& x <= info[i].x_org + info[i].width
&& info[i].y_org <= y
&& y <= info[i].y_org + info[i].height)
2015-04-04 09:25:57 +00:00
break;
if (i == count) return;
if (x + w > info[i].x_org + info[i].width)
x = info[i].x_org + info[i].width - w - BORDER_WIDTH - 1;
if (x < info[i].x_org) x = info[i].x_org;
if (y + h > info[i].y_org + info[i].height)
y = info[i].y_org + info[i].height - h - BORDER_WIDTH - 1;
if (y < info[i].y_org) y = info[i].y_org;
2015-04-04 22:15:44 +00:00
2015-04-04 09:25:57 +00:00
XMoveWindow(display, win, x, y);
}
}
void read_input() {
2015-04-08 04:01:50 +00:00
FcChar8 line[512];
int l;
option *o, *head;
head = malloc(sizeof(option));
head->next = NULL;
o = head;
while (fgets(line, sizeof(line), stdin)) {
o->next = malloc(sizeof(option));
o->next->prev = o;
o = o->next;
2015-04-08 04:01:50 +00:00
l = strlen(line);
o->text = calloc(sizeof(char), l);
strncpy(o->text, line, l - 1);
o->text[l - 1] = '\0';
o->next = NULL;
}
options = head->next;
options->prev = NULL;
free(head);
update_valid_options();
}
int main(int argc, char *argv[]) {
XSetWindowAttributes attributes;
XWindowAttributes window_attributes;
Visual *vis;
Colormap cmap;
XEvent ev;
int i;
2015-04-04 22:15:44 +00:00
char *bg_name = default_bg;
char *fg_name = default_fg;
char *fontstr = default_fontstr;
cursor = 0;
2015-04-04 09:25:57 +00:00
heading = "";
2015-04-08 04:01:50 +00:00
text = calloc(MAX_LEN, sizeof(FcChar8));
valid = NULL;
current = NULL;
for (i = 1; i < argc; i++) {
if (strcmp(argv[i], "-h") == 0) {
usage();
return 0;
} else if (strcmp(argv[i], "-a") == 0) {
exit_on_one = 1;
} else if (strcmp(argv[i], "-c") == 0) {
complete_on_exit = 1;
} else if (strcmp(argv[i], "-n") == 0) {
complete_on_exit = 1;
input_bar = 0;
} else if (strcmp(argv[i], "-e") == 0) {
read_options = 0;
} else if (strcmp(argv[i], "-p") == 0) {
i++;
x = atoi(strsep(&argv[i], ","));
y = atoi(argv[i]);
} else if (strcmp(argv[i], "-ap") == 0) {
absolute_position = 1;
2015-04-04 22:15:44 +00:00
} else if (strcmp(argv[i], "-fg") == 0) {
fg_name = argv[++i];
} else if (strcmp(argv[i], "-bg") == 0) {
bg_name = argv[++i];
} else if (strcmp(argv[i], "-fn") == 0) {
fontstr = argv[++i];
} else {
2015-04-04 09:25:57 +00:00
heading = argv[i];
}
}
if (read_options)
read_input();
display = XOpenDisplay(NULL);
root = RootWindow(display, 0);
screen = DefaultScreen(display);
vis = XDefaultVisual(display, screen);
cmap = DefaultColormap(display, screen);
if (!XftColorAllocName(display, vis, cmap, fg_name, &fg))
die("Failed to allocate foreground color");
if (!XftColorAllocName(display, vis, cmap, bg_name, &bg))
die("Failed to allocate background color");
/* Setup and map window */
attributes.border_pixel = fg.pixel;
2015-04-04 09:51:38 +00:00
attributes.background_pixel = bg.pixel;
attributes.override_redirect = True;
attributes.event_mask = ExposureMask | KeyPressMask | ButtonPressMask;
win = XCreateWindow(display, root,
0, 0, 10, 10, BORDER_WIDTH,
DefaultDepth(display, 0),
CopyFromParent, CopyFromParent,
2015-04-08 04:01:50 +00:00
CWBackPixel | CWOverrideRedirect | CWEventMask
| CWBorderPixel,
&attributes);
xim = XOpenIM(display, NULL, NULL, NULL);
xic = XCreateIC(xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
XNClientWindow, win, XNFocusWindow, win, NULL);
gc = XCreateGC(display, win, 0, 0);
buf = XCreatePixmap(display, win, 10, 10, DefaultDepth(display, screen));
draw = XftDrawCreate(display, buf, vis, cmap);
2015-04-04 22:15:44 +00:00
load_font(fontstr);
2015-04-04 09:25:57 +00:00
heading_width = text_width(heading);
cursor_width = text_width("_");
update_size();
set_position();
2015-04-04 09:25:57 +00:00
if (!absolute_position)
keep_in_screen();
render();
2015-04-04 22:15:44 +00:00
2015-04-04 09:25:57 +00:00
XMapWindow(display, win);
grab_keyboard_pointer();
2015-04-04 22:15:44 +00:00
while (!XNextEvent(display, &ev)) {
if (ev.type == KeyPress)
handle_key(ev.xkey);
else if (ev.type == ButtonPress)
handle_button(ev.xbutton);
else if (ev.type == Expose) {
XRaiseWindow(display, win);
render();
}
}
XUngrabKeyboard(display, CurrentTime);
XUngrabPointer(display, CurrentTime);
XFreeGC(display, gc);
XFreePixmap(display, buf);
XFreePixmap(display, nullpixmap);
return 0;
}