shithub: dmenu

ref: 1fd26030655c541e7c78d4e0db64ed6725d7cc73
dir: dmenu/dmenu.c

View raw version
#include <u.h>
#include <libc.h>
#include <bio.h>
#include <draw.h>
#include <event.h>
#include <keyboard.h>

#define Ctl(c) ((c) - 64)

enum
{
	BACK,
	HBACK,
	TEXT,
	HTEXT,
	NCOLORS
};

Image *color[NCOLORS];
char **lines, **matches, *buffer;
char prompt[64] = " > ";
usize nlines, nmatches, scroll;
Rune kinput[512];
int selected;

static void
readbuffer(void)
{
	Biobuf *bp;
	char *line, *s;

	if((bp = Bfdopen(0, OREAD)) == nil)
		sysfatal("setting buffering on fd0: %r");
	werrstr("empty");
	if((buffer = Brdstr(bp, '\0', 1)) == nil)
		sysfatal("reading input lines: %r");

	for(line = s = buffer; s = strchr(s, '\n'); line = ++s){
		*s = '\0';
		if((lines = realloc(lines, ++nlines * sizeof *lines)) == nil)
			sysfatal("malloc: %r");
		lines[nlines-1] = line;
	}

	if((matches = malloc(nlines * sizeof *lines)) == nil)
		sysfatal("malloc: %r");
	memmove(matches, lines, nlines * sizeof *lines);
	nmatches = nlines;
}

static Point
linetopoint(int ln)
{
	ln -= scroll;
	return Pt(screen->r.min.x, screen->r.min.y + (ln + 2) * font->height);
}

static int
pointtoline(Point pt)
{
	return (pt.y - screen->r.min.y) / font->height - 2 + scroll;
}

static void
drawbackground(Point pt, Image *color)
{
	draw(screen,
	  Rect(screen->r.min.x, pt.y, screen->r.max.x, pt.y + font->height),
	  color, nil, ZP);
}

static Point
tabstring(Image *dst, Point dp, Image *src, Point sp, Font *f, char *s)
{
	Rune r[2] = {L'0', L'\0'};
	int n, w0, x;
	Point op;

	op = dp;
	w0 = stringwidth(f, "0");
	for(; *s && (n = chartorune(r, s)) > 0; s += n){
		if(r[0] == '\t'){
			x = 8 * w0 - (dp.x - op.x) % (8 * w0);
			sp.x += x;
			dp.x += x;
		}else{
			dp = runestring(dst, dp, src, sp, f, r);
		}
	}
	return dp;
}

static void
drawprompt(void)
{
	Point pt;
	char buf[512];

	snprint(buf, sizeof buf, "%s%S▏", prompt, kinput);
	pt = Pt(screen->r.max.x, screen->r.min.y + font->height * 2);
	draw(screen, Rpt(screen->r.min, pt), color[BACK], nil, ZP);
	pt = Pt(screen->r.min.x, screen->r.min.y + font->height);
	tabstring(screen, pt, color[TEXT], ZP, font, buf);
}

static int
drawline(int ln)
{
	Point pt = linetopoint(ln);
	Image *bgcolor, *txcolor;

	if(ln < scroll)
		return 1;
	if(ln == selected){
		bgcolor = color[HBACK];
		txcolor = color[HTEXT];
	}else{
		bgcolor = color[BACK];
		txcolor = color[TEXT];
	}
	pt.x += stringwidth(font, prompt);
	drawbackground(pt, bgcolor);
	if(ln < nmatches)
		tabstring(screen, pt, txcolor, ZP, font, matches[ln]);
	return pt.y < screen->r.max.y;
}

void
eresized(int new)
{
	int i;

	if(new && getwindow(display, Refnone) < 0)
		sysfatal("resize failed: %r");

	drawprompt();
	for(i = scroll; drawline(i); i++);
}

int
match(char *s, char **words, int nwords)
{
	char **w;

	if(nwords == 0)
		return 1;
	for(w = words; w < words + nwords; w++)
		if(cistrstr(s, *w) == nil)
			return 0;
	return 1;
}

void
filter(char **lines, int nlines)
{
	char *buf, *words[64], **l, **m;
	int nwords, old;

	if((buf = smprint("%S", kinput)) == nil)
		sysfatal("malloc");
	nwords = tokenize(buf, words, sizeof words / sizeof *words);

	old = nmatches;
	nmatches = 0;
	m = matches;
	for(l = lines; l < lines + nlines; l++)
		if(match(*l, words, nwords)){
			*m++ = *l;
			nmatches++;
		}

	selected = 0;
	scroll = 0;
	free(buf);
	if (nmatches == old)  /* optimization */
		drawprompt();
	else
		eresized(0);
}

static void
kadd(Rune r)
{
	int len = runestrlen(kinput);

	if(len == sizeof kinput / sizeof *kinput)
		return;
	kinput[len++] = r;
	kinput[len] = L'\0';

	filter(matches, nmatches);
}

static void
kbbackspace(void)
{
	usize len = runestrlen(kinput);

	if(len == 0)
		return;
	kinput[len - 1] = L'\0';

	filter(lines, nlines);
}

static void
kbdelword(void)
{
	usize len = runestrlen(kinput);

	if(len == 0)
		return;
	while(len > 0 && isspacerune(kinput[len-1]))
		len--;
	while(len > 0 && !isspacerune(kinput[len-1]))
		len--;
	kinput[len] = L'\0';

	filter(lines, nlines);
}

static void
kbclear(void)
{
	kinput[0] = L'\0';

	filter(lines, nlines);
}

static void
kbmove(int n)
{
	int old = selected;

	if(selected + n < 0)
		selected = 0;
	else if(selected + n >= nmatches)
		selected = nmatches - 1;
	else
		selected += n;

	drawline(old);
	drawline(selected);
}

static void
kbscroll(int percent)
{
	int ln = Dy(screen->r) / font->height * percent / 100;

	if(ln < 0 && abs(ln) > scroll)
		scroll = 0;
	else if(ln > 0 && scroll + ln >= nmatches)
		scroll = nmatches - 1 + (nmatches > 0);
	else
		scroll += ln;

	eresized(0);
}

static void
mselect(Point pt)
{
	int old, new = pointtoline(pt);

	if(new < 0)
		new = 0;
	if(nmatches > 0 && new >= nmatches)
		new = nmatches - 1;
	if(new != selected){
		old = selected;
		selected = new;
		drawline(old);
		drawline(new);
	}
}

static void
usage(void)
{
	print("usage: %s [-b] [-p prompt] <choices\n", argv0);
	exits("usage");
}

void
main(int argc, char **argv)
{
	Event e;
	int bflag = 0;

	ARGBEGIN {
	case 'b':
		bflag = 1;
		break;
	case 'p':
		snprint(prompt, sizeof prompt, " %s> ", EARGF(usage()));
		break;
	default:
		usage();
	} ARGEND;

	readbuffer();

	if(initdraw(nil, nil, "linesel") < 0)
		sysfatal("initdraw: %r");

	if(bflag){
		color[TEXT] = display->white;
		color[BACK] = display->black;
	}else{
		color[TEXT] = display->black;
		color[BACK] = display->white;
	}
	color[HTEXT] = display->black;
	color[HBACK] = allocimage(display,
	  Rect(0,0,1,1), screen->chan, 1, DPaleyellow);

	eresized(0);

	einit(Emouse|Ekeyboard);
	for(;;){
		switch(event(&e)){
		case -1:
			sysfatal("watching events: %r");

		case Ekeyboard:
			switch(e.kbdc){
			case Kdel:
				exits("interrupted with Del");
			case '\n':
				goto End;
			case Kbs:
				kbbackspace();
				break;
			case Ctl('W'):
				kbdelword();
				break;
			case Ctl('U'):
				kbclear();
				break;
			case Ctl('P'):
			case Kup:
				kbmove(-1);
				break;
			case Ctl('N'):
			case Kdown:
				kbmove(+1);
				break;
			case Kpgdown:
				kbscroll(+40);
				break;
			case Kpgup:
				kbscroll(-40);
				break;
			default:
				kadd(e.kbdc);
				break;
			}
			break;

		case Emouse:
			if(e.mouse.buttons&1)
				mselect(e.mouse.xy);
			if(e.mouse.buttons&4)
				goto End;
			if(e.mouse.buttons&8)
				kbscroll(-40);
			if(e.mouse.buttons&16)
				kbscroll(+40);
			break;
		}
	}
End:
	if(nmatches > 0)
		print("%s\n", matches[selected]);
	exits(nil);
}