shithub: vdict

Download patch

ref: 0281f3466bd385123ea7608832e8bc6deb54dd78
author: phil9 <telephil9@gmail.com>
date: Tue Mar 22 16:26:46 EDT 2022

initial import

--- /dev/null
+++ b/LICENSE
@@ -1,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 phil9 <telephil9@gmail.com>
+
+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.
--- /dev/null
+++ b/README.md
@@ -1,0 +1,21 @@
+# vdict
+A visual DICT client for plan9
+
+![vdict](vdict.png)
+
+Enter the word you want to search in the entry and press `Enter`.  
+Coloured words are links that can be clicked to jump to their definition.
+
+## Usage 
+```sh
+% mk install
+% vdict -h <host> [-p <port>]
+```
+You can use `dict.org` for instance.
+
+## License
+MIT
+
+## Bugs
+You tell me!
+
--- /dev/null
+++ b/a.h
@@ -1,0 +1,86 @@
+typedef struct Dictc Dictc;
+typedef struct Dvec  Dvec;
+typedef struct Element Element;
+typedef struct Definition Definition;
+typedef struct Entry Entry;
+typedef struct Cols Cols;
+
+#pragma incomplete Dvec;
+
+struct Dictc
+{
+	int     fd;
+	Biobuf* bin;
+	Dvec*   db;
+	Dvec*   strat;
+};
+
+struct Element
+{
+	char *name;
+	char *desc;
+};
+
+struct Definition
+{
+	char *db;
+	char *text;
+};
+
+struct Cols
+{
+	Image *back;
+	Image *text;
+	Image *focus;
+	Image *sel;
+	Image *scrl;
+};
+
+struct Entry
+{
+	Rectangle r;
+	ushort state;
+	int tickx;
+	int p0, p1;
+	int len;
+	int size;
+	int buttons;
+	char *text;
+	Channel *c;
+	Cols *cols;
+};
+
+/* DICT client */
+#define Dfirstmatch "!"
+#define Dallmatches "*"
+
+Dictc* dictdial(const char*, int);
+void   dictquit(Dictc*);
+Dvec*  dictdefine(Dictc*, char*, char*);
+
+usize  dvlen(Dvec*);
+void*  dvref(Dvec*, usize);
+
+/* dview */
+void dviewinit(Channel*, Cols*);
+void dviewresize(Rectangle);
+void dviewredraw(void);
+void dviewmouse(Mouse);
+void dviewkey(Rune);
+void dviewset(Dvec*);
+
+/* entry */
+void entryinit(Entry*, Cols*);
+void entryresize(Entry*, Rectangle);
+void entryredraw(Entry*);
+int  entrymouse(Entry*, Mouse);
+void entrykey(Entry*, Rune);
+int  entryhasfocus(Entry*);
+void entryfocus(Entry*, int);
+void entrysettext(Entry*, char*);
+
+/* utils */
+void *emalloc(ulong);
+void *erealloc(void*, ulong);
+
+
--- /dev/null
+++ b/dictc.c
@@ -1,0 +1,349 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <String.h>
+#include <draw.h>
+#include <thread.h>
+#include "a.h"
+
+typedef struct Response Response;
+
+struct Response
+{
+	int code;
+	char *msg;
+};
+
+struct Dvec
+{
+	void **elts;
+	usize  len;
+	usize  sz;
+};
+
+enum
+{
+	Eok,
+	Eeof,
+	Eunexpected,
+	Ebadformat,
+	Enodb,
+	Enostrat,
+};
+
+const char* Errors[] = {
+	[Eeof]        = "no response from server",
+	[Eunexpected] = "unexpected response from server",
+	[Ebadformat]  = "bad response format",
+	[Enodb]       = "server does not have any database",
+	[Enostrat]    = "server doest not have any strategy",
+};
+
+Dvec*
+mkdvec(usize size)
+{
+	Dvec *v;
+
+	v = emalloc(sizeof *v);
+	v->len  = 0;
+	v->sz   = size;
+	v->elts = emalloc(size * sizeof(void*));
+	return v;
+}
+
+void
+dvadd(Dvec *v, void *p)
+{
+	if(v->len == v->sz){
+		v->sz  *= 1.5;
+		v->elts = erealloc(v->elts, v->sz * sizeof(void*));
+	}
+	v->elts[v->len++] = p;
+}
+
+usize
+dvlen(Dvec *v)
+{
+	return v->len;
+}
+
+void*
+dvref(Dvec *v, usize i)
+{
+	return v->elts[i];
+}
+
+int
+readstatus(char *s)
+{
+	char *e;
+	long l;
+
+	l = strtol(s, &e, 10);
+	if(l == 0 || e == s)
+		return -1;
+	return l;
+}
+
+int
+expectline(Dictc *c, char *l)
+{
+	char *s;
+
+	s = Brdstr(c->bin, '\n', 1);
+	if(s == nil)
+		return Eeof;
+	if(strncmp(s, l, strlen(l)) == 0)
+		return Eok;
+	return Ebadformat;
+}
+
+int
+sendcmd(Dictc *c, const char *cmd, Response *r)
+{
+	char *s;
+
+	write(c->fd, cmd, strlen(cmd));
+	write(c->fd, "\n", 1);
+	s = Brdstr(c->bin, '\n', 1);
+	if(s == nil)
+		return Eeof;
+	if(r != nil){
+		r->code = readstatus(s);
+		if(r->code == -1){
+			free(s);
+			return Eunexpected;
+		}
+		r->msg = strdup(s+4);
+	}
+	free(s);
+	return Eok;
+}
+
+int
+showdb(Dictc *c)
+{
+	Response r;
+	Element *e;
+	char *s, *p;
+	int rc, n, i;
+
+	rc = sendcmd(c, "SHOW DB", &r);
+	if(rc != Eok)
+		return rc;
+	if(r.code == 554)
+		return Enodb;
+	else if(r.code != 110)
+		return Eunexpected;
+	n = readstatus(r.msg);
+	free(r.msg);
+	c->db = mkdvec(n);
+	for(i = 0; i < n; i++){
+		s = Brdstr(c->bin, '\n', 1);
+		if(s == nil)
+			return Eeof;
+		p = strchr(s, ' ');
+		if(p == nil)
+			return Ebadformat;
+		e = emalloc(sizeof(Element));
+		p += 2; /* skip <space>" */
+		p[strlen(p) - 2] = 0; /* remove "\r */
+		e->desc = strdup(p);
+		*p = '\0';
+		e->name = strdup(s);
+		dvadd(c->db, e);
+		free(s);
+	}
+	if((n = expectline(c, ".")) != Eok)
+		return n;
+	if((n = expectline(c, "250 ok")) != Eok)
+		return n;
+	return Eok;
+}
+
+int
+showstrat(Dictc *c)
+{
+	Response r;
+	Element *e;
+	char *s, *p;
+	int rc, n, i;
+
+	rc = sendcmd(c, "SHOW STRAT", &r);
+	if(rc != Eok)
+		return rc;
+	if(r.code == 555)
+		return Enostrat;
+	else if(r.code != 111)
+		return Eunexpected;
+	n = readstatus(r.msg);
+	free(r.msg);
+	c->strat = mkdvec(n);
+	for(i = 0; i < n; i++){
+		s = Brdstr(c->bin, '\n', 1);
+		if(s == nil)
+			return Eeof;
+		p = strchr(s, ' ');
+		if(p == nil)
+			return Ebadformat;
+		e = emalloc(sizeof(Element));
+		p += 2; /* skip <space>" */
+		p[strlen(p) - 2] = 0; /* remove "\r */
+		e->desc = strdup(p);
+		*p = '\0';
+		e->name = strdup(s);
+		dvadd(c->strat, e);
+		free(s);
+	}
+	if((n = expectline(c, ".")) != Eok)
+		return n;
+	if((n = expectline(c, "250 ok")) != Eok)
+		return n;
+	return Eok;
+}
+
+void
+freedictc(Dictc *c)
+{
+	Element *e;
+	int i;
+
+	Bterm(c->bin);
+	close(c->fd);
+	if(c->db != nil){
+		for(i = 0; i < dvlen(c->db); i++){
+			e = dvref(c->db, i);
+			free(e->name);
+			free(e->desc);
+			free(e);
+		}
+		free(c->db);
+	}
+	free(c);
+}
+
+Dictc*
+dictdial(const char *addr, int port)
+{
+	Dictc *c;
+	char *s, buf[255];
+	int n;
+
+	if(port == 0)
+		port = 2628;
+	snprint(buf, sizeof buf, "tcp!%s!%d", addr, port);
+	c = malloc(sizeof *c);
+	if(c == nil)
+		sysfatal("malloc: %r");
+	c->fd = dial(buf, nil, nil, nil);
+	if(c->fd <= 0)
+		sysfatal("dial: %r");
+	c->bin = Bfdopen(c->fd, OREAD);
+	if(c->bin == nil)
+		sysfatal("Bfdopen: %r");
+	s = Brdstr(c->bin, '\n', 1);
+	if(s == nil){
+		werrstr("no status sent by server");
+		freedictc(c);
+		return nil;
+	}
+	n = showdb(c);
+	if(n != Eok){
+		werrstr(Errors[n]);
+		freedictc(c);
+		return nil;
+	}
+	n = showstrat(c);
+	if(n != Eok){
+		werrstr(Errors[n]);
+		freedictc(c);
+		return nil;
+	}
+	return c;
+}
+
+void
+dictquit(Dictc *c)
+{
+	sendcmd(c, "QUIT", nil);
+	freedictc(c);
+}
+
+Definition*
+parsedefinition(Dictc *c)
+{
+	Definition *d;
+	char *s;
+	String *sb;
+	int n;
+
+	s = Brdstr(c->bin, '\n', 1);
+	if(s == nil){
+		werrstr(Errors[Eeof]);
+		return nil;
+	}
+	n = readstatus(s);
+	free(s);
+	if(n != 151){
+		werrstr(Errors[Eunexpected]);
+		return nil;
+	}
+	sb = s_newalloc(255);
+	for(;;){
+		s = Brdstr(c->bin, '\n', 1);
+		if(s == nil){
+			s_free(sb);
+			werrstr(Errors[Eeof]);
+			return nil;
+		}
+		if(*s == '.'){
+			free(s);
+			break;
+		}
+		s[Blinelen(c->bin) - 1] = '\n'; /* replace \r with \n */
+		s_append(sb, s);
+		free(s);
+	}
+	s_terminate(sb);
+	d = emalloc(sizeof *d);
+	d->text = strdup(s_to_c(sb));
+	s_free(sb);
+	return d;
+}
+
+Dvec*
+dictdefine(Dictc* c, char *db, char *word)
+{
+	Dvec *v;
+	Response r;
+	Definition *d;
+	char buf[1024];
+	int rc, n, i;
+
+	snprint(buf, sizeof buf, "DEFINE %s \"%s\"", db, word);
+	rc = sendcmd(c, buf, &r);
+	if(rc != Eok){
+		werrstr(Errors[rc]);
+		return nil;
+	}
+	if(r.code == 552)
+		return mkdvec(1);
+	if(r.code != 150){
+		werrstr(Errors[Eunexpected]);
+		return nil;
+	}
+	n = readstatus(r.msg);
+	v = mkdvec(n);
+	for(i = 0; i < n; i++){
+		d = parsedefinition(c);
+		if(d == nil)
+			return nil; /* FIXME: cleanup vec */
+		dvadd(v, d);
+	}
+	if((n = expectline(c, "250 ok")) != Eok){
+		werrstr(Errors[n]);
+		return nil;
+	}
+	return v;
+}
+
--- /dev/null
+++ b/dview.c
@@ -1,0 +1,286 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <bio.h>
+#include "a.h"
+
+typedef struct Box Box;
+typedef struct Link Link;
+
+struct Box
+{
+	Rectangle r;
+	Rectangle sr;
+	Image *b;
+};
+
+struct Link
+{
+	Rectangle r;
+	char text[255];
+};
+
+enum
+{
+	Padding = 4,
+	Scrollwidth = 12,
+};
+
+Channel *chan;
+Dvec *defs;
+Box **boxes;
+usize nboxes;
+Link links[1024];
+usize nlinks;
+Cols *cols;
+Rectangle sr;
+Rectangle scrollr;
+Rectangle boxr;
+int totalh;
+int viewh;
+int offset;
+int scrollsize;
+
+Box*
+renderbox(Definition *d)
+{
+	Box *b;
+	int i, l, n, w, mw, inlink, cl;
+	Point p, lp;
+	Image *c;
+
+	n = 0;
+	w = 0;
+	mw = 0;
+	l = strlen(d->text);
+	for(i = 0; i < l; i++){
+		if(d->text[i] == '\n'){
+			++n;
+			if(w > mw)
+				mw = w;
+			w = 0;
+		}else{
+			w += stringnwidth(font, d->text+i, 1);
+		}
+	}
+	b = emalloc(sizeof *b);
+	b->r = Rect(0, 0, Padding + mw + Padding, Padding+(n+1)*font->height+Padding);
+	b->b = allocimage(display, b->r, screen->chan, 0, DNofill);
+	draw(b->b, b->r, cols->back, nil, ZP);
+	p = Pt(Padding, Padding);
+	inlink = 0;
+	cl = 0;
+	for(i = 0; i < l; i++){
+		switch(d->text[i]){
+		case '\n':
+			p.x = Padding;
+			p.y += font->height;
+			break;
+		case '{':
+			cl = 0;
+			lp = p;
+			inlink = 1;
+			break;
+		case '}':
+			links[nlinks].r = Rpt(lp, addpt(p, Pt(0, font->height)));
+			links[nlinks].text[cl] = '\0';
+			nlinks += 1;
+			cl = 0;
+			inlink = 0;
+			break;
+		default:
+			c = cols->text;
+			if(inlink){
+				c = cols->focus;
+				links[nlinks].text[cl++] = d->text[i];
+			}
+			p = stringn(b->b, p, c, ZP, font, d->text + i, 1);
+			break;
+		}
+	}
+	return b;
+}
+
+void
+layout(void)
+{
+	Box *b;
+	int i;
+	Point p;
+
+	totalh = 0;
+	p = addpt(boxr.min, Pt(Padding, Padding));
+	for(i = 0; i < nboxes; i++){
+		b = boxes[i];
+		b->sr = rectaddpt(b->r, p);
+		p.y += Dy(b->r) + Padding;
+		totalh += Dy(b->r) + Padding;
+	}
+	scrollsize = 10*totalh/100.0;
+}
+
+void
+dviewset(Dvec *d)
+{
+	Definition *def;
+	int i;
+
+	if(defs != nil){
+		for(i = 0; i < nboxes; i++){
+			freeimage(boxes[i]->b);
+			free(boxes[i]);
+		}
+		nboxes = 0;
+		for(i = 0; i < dvlen(defs); i++){
+			def = dvref(defs, i);
+			free(def->text);
+			free(def);
+		}
+		free(defs);
+	}
+	nlinks = 0;
+	defs = d;
+	nboxes = dvlen(defs);
+	boxes = emalloc(nboxes * sizeof(Box*));
+	for(i = 0; i < nboxes; i++){
+		def = dvref(defs, i);
+		boxes[i] = renderbox(def);
+	}
+	layout();
+}
+
+void
+dviewredraw(void)
+{
+	Box *b;
+	Rectangle clipr, scrposr;
+	int i, h, y, ye, vmin, vmax;
+
+	clipr = screen->clipr;
+	draw(screen, sr, cols->back, nil, ZP);
+	draw(screen, scrollr, cols->scrl, nil, ZP);
+	border(screen, scrollr, 1, cols->text, ZP);
+	if(viewh < totalh){
+		h = ((double)viewh/totalh) * Dy(scrollr);
+		y = ((double)offset/totalh) * Dy(scrollr);
+		ye = scrollr.min.y + y + h - 1;
+		if(ye >= scrollr.max.y)
+			ye = scrollr.max.y - 1;
+		scrposr = Rect(scrollr.min.x + 1, scrollr.min.y + y + 1, scrollr.max.x - 1, ye);
+	}else
+		scrposr = insetrect(scrollr, -1);
+	draw(screen, scrposr, cols->back, nil, ZP);
+	replclipr(screen, 0, boxr);
+	vmin = boxr.min.y + offset;
+	vmax = boxr.max.y + offset;
+	if(boxes != nil){
+		for(i = 0; i < nboxes; i++){
+			b = boxes[i];
+			if(b->sr.min.y <= vmax && b->sr.max.y >= vmin)
+				draw(screen, rectaddpt(b->sr, Pt(0, -offset)), b->b, nil, ZP);
+		}
+	}
+	replclipr(screen, 0, clipr);
+}
+
+void
+dviewresize(Rectangle r)
+{
+	sr = r;
+	scrollr = sr;
+	scrollr.min.x += Padding;
+	scrollr.max.x = scrollr.min.x + Padding + Scrollwidth;
+	scrollr.max.y -= Padding;
+	boxr = sr;
+	boxr.min.x = scrollr.max.x + Padding;
+	boxr.max.x -= Padding;
+	boxr.max.y -= Padding;
+	viewh = Dy(boxr);
+	if(boxes != nil)
+		layout();
+}
+
+void
+scroll(int d)
+{
+	if(d < 0 && offset <= 0)
+			return;
+	if(d > 0 && offset + viewh > totalh)
+			return;
+	offset += d;
+	if(offset < 0)
+		offset = 0;
+	if((offset + viewh ) > totalh)
+		offset = totalh - viewh;
+	dviewredraw();
+	flushimage(display, 1);
+}
+
+void
+clicklink(Point p)
+{
+	int i;
+
+	p = subpt(p, addpt(boxr.min, Pt(0, offset)));
+	for(i = 0; i < nlinks; i++){
+		if(ptinrect(p, links[i].r)){
+			nbsendp(chan, strdup(links[i].text));
+			return;
+		}
+	}
+}
+
+void
+dviewmouse(Mouse m)
+{
+	if(!ptinrect(m.xy, sr))
+		return;
+	if(m.buttons == 1)
+		clicklink(m.xy);
+	else if(m.buttons == 8)
+		scroll(-scrollsize);
+	else if(m.buttons == 16)
+		scroll(scrollsize);
+}
+
+void
+dviewkey(Rune k)
+{
+	switch(k){
+	case Kup:
+		scroll(-scrollsize);
+		break;
+	case Kdown:
+		scroll(scrollsize);
+		break;
+	case Kpgup:
+		scroll(-viewh);
+		break;
+	case Kpgdown:
+		scroll(viewh);
+		break;
+	case Khome:
+		scroll(-totalh);
+		break;
+	case Kend:
+		scroll(totalh);
+		break;
+	}
+}
+
+
+void
+dviewinit(Channel *ch, Cols *c)
+{
+	chan = ch;
+	defs = nil;
+	boxes = nil;
+	nboxes = 0;
+	nlinks = 0;
+	totalh = -1;
+	offset = 0;
+	cols = c;
+}
--- /dev/null
+++ b/entry.c
@@ -1,0 +1,419 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include <thread.h>
+#include <ctype.h>
+#include <bio.h>
+#include "a.h"
+
+enum
+{
+	Senabled = 1 << 0,
+	Sfocused = 1 << 1,
+};
+
+enum
+{
+	Padding = 4,
+};
+
+static void einsert(Entry *entry, char *s);
+static void edelete(Entry *entry, int bs);
+
+static char *menu2str[] =
+{
+	"cut",
+	"paste",
+	"snarf",
+	0 
+};
+
+enum
+{
+	Mcut,
+	Mpaste,
+	Msnarf,
+};
+static Menu menu2 = { menu2str };
+static Image *tick = nil;
+
+int
+min(int x, int y)
+{
+	return x <= y ? x : y;
+}
+
+int
+max(int x, int y)
+{
+	return x >= y ? x : y;
+}
+
+static Image*
+createtick(Image *bg, Image *fg)
+{
+	enum { Tickw = 3 };
+	Image *t;
+
+	t = allocimage(display, Rect(0, 0, Tickw, font->height), screen->chan, 0, DWhite);
+	if(t == nil)
+		return 0;
+	/* background color */
+	draw(t, t->r, bg, nil, ZP);
+	/* vertical line */
+	draw(t, Rect(Tickw/2, 0, Tickw/2+1, font->height), fg, nil, ZP);
+	/* box on each end */
+	draw(t, Rect(0, 0, Tickw, Tickw), fg, nil, ZP);
+	draw(t, Rect(0, font->height-Tickw, Tickw, font->height), fg, nil, ZP);
+	return t;
+}
+
+void
+entryinit(Entry *e, Cols *cols)
+{
+	if(tick == nil)
+		tick = createtick(cols->back, cols->text);
+	e->state = Senabled;
+	e->buttons = 0;
+	e->tickx = 0;
+	e->size = 255;
+	e->text = emalloc(e->size * sizeof(char));
+	e->text[0] = 0;
+	e->p0 = 0;
+	e->p1 = 0;
+	e->len = 0;
+	e->c = chancreate(sizeof(char*), 1);
+	e->cols = cols;
+}
+
+int
+entryhasfocus(Entry *e)
+{
+	return (e->state & Sfocused);
+}
+
+static void
+entryunfocus(Entry *e)
+{
+	if(!entryhasfocus(e))
+		return;
+	e->state ^= Sfocused;
+	e->buttons = 0;
+	e->p0 = e->len;
+	e->p1 = e->len;
+	entryredraw(e);
+}
+
+void
+entryfocus(Entry *e, int sel)
+{
+	if(entryhasfocus(e))
+		return;
+	e->state |= Sfocused;
+	if(sel){
+		e->p0 = 0;
+		e->p1 = e->len;
+	}
+	entryredraw(e);
+}
+
+void
+entrysettext(Entry *e, const char *text)
+{
+	int l;
+	
+	l = strlen(text);
+	if(l >= e->size) {
+		e->size = l;
+		e->text = erealloc(e->text, e->size * sizeof(char));
+	}
+	strncpy(e->text, text, l);
+	e->text[l] = 0;
+	e->len = l;
+	e->p0 = e->len;
+	e->p1 = e->p0;
+	e->tickx = stringnwidth(font, e->text, e->len);
+}
+
+void 
+entryresize(Entry *e, Rectangle r)
+{
+	e->r = r;
+}
+
+void
+entryredraw(Entry *e)
+{
+	Rectangle r, clipr;
+	Point p;
+	int y, sels, sele;
+
+	clipr = screen->clipr;
+	replclipr(screen, 0, e->r);
+	draw(screen, e->r, e->cols->back, nil, ZP);
+	if(entryhasfocus(e))
+		border(screen, e->r, 1, e->cols->focus, ZP);
+	else
+		border(screen, e->r, 1, e->cols->text, ZP);
+	y = (Dy(e->r) - font->height) / 2;
+	p = Pt(e->r.min.x + Padding, e->r.min.y + y);
+	stringn(screen, p, e->cols->text, ZP, font, e->text, e->len);
+	if (e->p0 != e->p1) {
+		sels = min(e->p0, e->p1);
+		sele = max(e->p0, e->p1);
+		p.x += stringnwidth(font, e->text, sels);
+		stringnbg(screen, p, e->cols->text, ZP, font, e->text+sels, sele-sels, e->cols->sel, ZP);
+	} else if (e->state & Sfocused) {
+		e->tickx = stringnwidth(font, e->text, e->p0);
+		p.x += e->tickx;
+		r = Rect(p.x, p.y, p.x + Dx(tick->r), p.y + Dy(tick->r));
+		draw(screen, r, tick, nil, ZP);
+	}	
+	flushimage(display, 1);
+	replclipr(screen, 0, clipr);
+}
+
+static int
+ptpos(Entry *e, Mouse m)
+{
+	int i, x, prev, cur;
+
+	x = m.xy.x - e->r.min.x - Padding;
+	prev = 0;
+	for(i = 0; i < e->len; i++){
+		cur = stringnwidth(font, e->text, i);
+		if ((prev+cur)/2 >= x){
+			i--;
+			break;
+		}else if (prev <= x && cur >= x)
+			break;
+		prev = cur;
+	}
+	return i;
+}
+
+static int
+issep(char c)
+{
+	return c == 0 || c == '/' || (!isalnum(c) && c != '-');
+}
+
+static void
+entryclicksel(Entry *e)
+{
+	int s, t;
+
+	if(e->p0 == 0)
+		e->p1 = e->len;
+	else if(e->p0 == e->len)
+		e->p0 = 0;
+	else{
+		s = e->p0;
+		t = e->p0;
+		while((s - 1) >= 0 && !issep(e->text[s - 1]))
+			--s;
+		while(t < e->len && !issep(e->text[t]))
+			++t;
+		e->p0 = s;
+		e->p1 = t;
+	}
+}
+
+int
+entrymouse(Entry *e, Mouse m)
+{
+	static int lastn = -1;
+	static ulong lastms = 0;
+	int in, n, sels, sele;
+	char *s;
+	usize len;
+
+	s = nil;
+	len = 0;
+	in = ptinrect(m.xy, e->r);
+	if(in && !e->buttons && m.buttons)
+		e->state |= Sfocused;
+	if(e->state & Sfocused){
+		n = ptpos(e, m);
+		if(!in && !e->buttons && m.buttons){
+			entryunfocus(e);
+			return -1;
+		}
+		if(m.buttons & 1){ /* holding left button */
+			sels = min(e->p0, e->p1);
+			sele = max(e->p0, e->p1);
+			if(m.buttons == (1|2) && e->buttons == 1){
+				if(sels != sele){
+					/* TODO: snarf */
+					edelete(e, 0);
+				}
+			}else if(m.buttons == (1|4) && e->buttons == 1){
+				/* TODO: paste */
+				//plan9_paste(&s, &len);
+				if(len > 0 && s != nil)
+					einsert(e, s);
+				free(s);
+			}else if(m.buttons == 1 && e->buttons <= 1){
+				e->p0 = n;
+				if (e->buttons == 0){
+					e->p1 = n;
+					if(n == lastn && lastms > 0 && (m.msec - lastms)<=250)
+						entryclicksel(e);
+				}
+			}
+			entryredraw(e);
+			lastn = n;
+			lastms = m.msec;
+		} else if (m.buttons & 2) {
+			//sels = min(e->p0, e->p1);
+			//sele = max(e->p0, e->p1);
+			/* TODO
+			//n = emenuhit(2, &e.mouse, &menu2);
+			n = -1;
+			switch(n) {
+			case Mcut:
+				if (sels != sele) {
+					plan9_snarf(entry->text+sels, sele-sels);
+					text_delete(entry, 0);
+				}
+				break;
+			case Mpaste:
+				plan9_paste(&s, &len);
+				if (len >= 0 && s != NULL)
+					text_insert(entry, s);
+				free(s);
+				break;
+			case Msnarf:
+				if (sels != sele) {
+					plan9_snarf(entry->text+sels, sele-sels);
+				}
+				break;
+			}
+			entryredraw(e);
+			*/
+		}
+		e->buttons = m.buttons;
+		return 0;
+	}
+	return -1;
+}
+
+void
+entrykey(Entry *e, Rune k)
+{
+	int sels, sele, n;
+	char s[UTFmax+1];
+
+	if(!entryhasfocus(e))
+		return;
+	sels = min(e->p0, e->p1);
+	sele = max(e->p0, e->p1);
+	switch (k) {
+	case Keof:
+	case '\n':
+		e->p0 = e->p1 = e->len;
+		nbsendp(e->c, strdup(e->text));
+		break;
+	case Knack:	/* ^U: delete selection, if any, and everything before that */
+		memmove(e->text, e->text + sele, e->len - sele);
+		e->len = e->len - sele;
+		e->p0 = 0;
+		e->text[e->len] = 0;
+		break;
+	case Kleft:
+		e->p0 = max(0, sels-1);
+		break;
+	case Kright:
+		e->p0 = min(e->len, sele+1);
+		break;
+	case Ksoh:	/* ^A: start of line */
+	case Khome:
+		e->p0 = 0;
+		break;
+	case Kenq:	/* ^E: end of line */
+	case Kend:
+		e->p0 = e->len;
+		break;
+	case Kdel:
+		edelete(e, 0);
+		break;
+	case Kbs:
+		edelete(e, 1);
+		break;
+	case Ketb:
+		while(sels > 0 && !isalnum(e->text[sels-1]))
+			sels--;
+		while(sels > 0 && isalnum(e->text[sels-1]))
+			sels--;
+		e->p0 = sels;
+		e->p1 = sele;
+		edelete(e, 0);
+		break;
+	case Kesc:
+		if (sels == sele) {
+			sels = e->p0 = 0;
+			sele = e->p1 = e->len;
+		}
+		/* TODO */
+		//plan9_snarf(e->text+sels, sele-sels);
+		edelete(e, 0);
+		break;
+	case 0x7: /* ^G: remove focus */
+		entryunfocus(e);
+		return;
+	default:
+		if(k < 0x20 || (k & 0xFF00) == KF || (k & 0xFF00) == Spec || (n = runetochar(s, &k)) < 1)
+			return;
+		s[n] = 0;
+		einsert(e, s);
+	}
+	e->p1 = e->p0;
+	entryredraw(e);
+}
+
+static void
+einsert(Entry *e, char *s)
+{
+	int sels, sele, n;
+	char *p;
+
+	n = strlen(s);
+	if(e->size <= e->len + n){
+		e->size = (e->len + n)*2 + 1;
+		if((p = realloc(e->text, e->size)) == nil)
+			return;
+		e->text = p;
+	}
+	sels = min(e->p0, e->p1);
+	sele = max(e->p0, e->p1);
+	if(sels != sele){
+		memmove(e->text + sels + n, e->text + sele, e->len - sele);
+		e->len -= sele - sels;
+		e->p0 = sels;
+	}else if (e->p0 != e->len)
+		memmove(e->text + e->p0 + n, e->text + e->p0, e->len - e->p0);
+	memmove(e->text + sels, s, n);
+	e->len += n;
+	e->p1 = sels;
+	e->p0 = sels + n;
+	e->text[e->len] = 0;		
+}
+
+static void
+edelete(Entry *e, int bs)
+{
+	int sels, sele;
+
+	sels = min(e->p0, e->p1);
+	sele = max(e->p0, e->p1);
+	if(sels == sele && sels == 0)
+		return;
+	memmove(e->text + sels - bs, e->text + sele, e->len - sele);
+	e->p0 = sels - bs;
+	e->len -= sele - sels + bs;
+	e->p1 = e->p0;
+	e->text[e->len] = 0;
+}
+
--- /dev/null
+++ b/mkfile
@@ -1,0 +1,9 @@
+</$objtype/mkfile
+
+BIN=/$objtype/bin
+TARG=vdict
+OFILES=vdict.$O dictc.$O dview.$O entry.$O theme.$O utils.$O
+HFILES=a.h
+
+</sys/src/cmd/mkone
+
--- /dev/null
+++ b/theme.c
@@ -1,0 +1,84 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include "theme.h"
+
+Image*
+ereadcol(char *s)
+{
+	Image *i;
+	char *e;
+	ulong c;
+
+	c = strtoul(s, &e, 16);
+	if(e == nil || e == s)
+		return nil;
+	c = (c << 8) | 0xff;
+	i = allocimage(display, Rect(0, 0, 1, 1), screen->chan, 1, c);
+	if(i == nil)
+		sysfatal("allocimage: %r");
+	return i;
+}
+
+Theme*
+loadtheme(void)
+{
+	Theme *theme;
+	Biobuf *bp;
+	char *s;
+
+	if(access("/dev/theme", AREAD) < 0)
+		return 0;
+	bp = Bopen("/dev/theme", OREAD);
+	if(bp == nil)
+		return 0;
+	theme = malloc(sizeof *theme);
+	if(theme == nil){
+		Bterm(bp);
+		return nil;
+	}
+	for(;;){
+		s = Brdstr(bp, '\n', 1);
+		if(s == nil)
+			break;
+		if(strncmp(s, "back", 4) == 0)
+			theme->back = ereadcol(s+5);
+		else if(strncmp(s, "high", 4) == 0)
+			theme->high = ereadcol(s+5);
+		else if(strncmp(s, "border", 6) == 0)
+			theme->border = ereadcol(s+7);
+		else if(strncmp(s, "text", 4) == 0)
+			theme->text = ereadcol(s+5);
+		else if(strncmp(s, "htext", 5) == 0)
+			theme->htext = ereadcol(s+6);
+		else if(strncmp(s, "title", 5) == 0)
+			theme->title = ereadcol(s+6);
+		else if(strncmp(s, "ltitle", 6) == 0)
+			theme->ltitle = ereadcol(s+7);
+		else if(strncmp(s, "hold", 4) == 0)
+			theme->hold = ereadcol(s+5);
+		else if(strncmp(s, "lhold", 5) == 0)
+			theme->lhold = ereadcol(s+6);
+		else if(strncmp(s, "palehold", 8) == 0)
+			theme->palehold = ereadcol(s+9);
+		else if(strncmp(s, "paletext", 8) == 0)
+			theme->paletext = ereadcol(s+9);
+		else if(strncmp(s, "size", 4) == 0)
+			theme->size = ereadcol(s+5);
+		else if(strncmp(s, "menuback", 8) == 0)
+			theme->menuback = ereadcol(s+9);
+		else if(strncmp(s, "menuhigh", 8) == 0)
+			theme->menuhigh = ereadcol(s+9);
+		else if(strncmp(s, "menubord", 8) == 0)
+			theme->menubord = ereadcol(s+9);
+		else if(strncmp(s, "menutext", 8) == 0)
+			theme->menutext = ereadcol(s+9);
+		else if(strncmp(s, "menuhtext", 5) == 0)
+			theme->menuhtext = ereadcol(s+6);
+		free(s);
+	}
+	Bterm(bp);
+	return theme;
+}
+
--- /dev/null
+++ b/theme.h
@@ -1,0 +1,25 @@
+typedef struct Theme Theme;
+
+struct Theme
+{
+	Image *back;
+	Image *high;
+	Image *border;
+	Image *text;
+	Image *htext;
+	Image *title;
+	Image *ltitle;
+	Image *hold;
+	Image *lhold;
+	Image *palehold;
+	Image *paletext;
+	Image *size;
+	Image *menubar;
+	Image *menuback;
+	Image *menuhigh;
+	Image *menubord;
+	Image *menutext;
+	Image *menuhtext;
+};
+
+Theme* loadtheme(void);
--- /dev/null
+++ b/utils.c
@@ -1,0 +1,29 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <draw.h>
+#include <thread.h>
+#include "a.h"
+
+void*
+emalloc(ulong size)
+{
+	void *p;
+
+	p = malloc(size);
+	if(p == nil)
+		sysfatal("malloc: %r");
+	return p;
+}
+
+void*
+erealloc(void *p, ulong size)
+{
+	void *q;
+
+	q = realloc(p, size);
+	if(q == nil)
+		sysfatal("realloc: %r");
+	return q;
+}
+
--- /dev/null
+++ b/vdict.c
@@ -1,0 +1,200 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+#include <draw.h>
+#include <mouse.h>
+#include <keyboard.h>
+#include "a.h"
+#include "theme.h"
+
+Dictc *dict;
+Mousectl *mc;
+Keyboardctl *kc;
+Entry *entry;
+Cols *cols;
+Rectangle searchr;
+Rectangle entryr;
+Rectangle viewr;
+
+enum
+{
+	Padding = 4,
+};
+
+void
+redraw(void)
+{
+	draw(screen, screen->r, cols->back, nil, ZP);
+	string(screen, addpt(searchr.min, Pt(Padding, 2*Padding+2)), cols->text, ZP, font, "Search:");
+	entryredraw(entry);
+	dviewredraw();
+	flushimage(display, 1);
+}
+
+void
+emouse(Mouse m)
+{
+	entrymouse(entry, m);
+	dviewmouse(m);
+}
+
+void
+ekeyboard(Rune k)
+{
+	switch(k){
+	case Kdel:
+		threadexitsall(nil);
+		break;
+	case Kstx:
+		if(!entryhasfocus(entry))
+			entryfocus(entry, 1);
+		break;
+	default:
+		entrykey(entry, k);
+		if(!entryhasfocus(entry))
+			dviewkey(k);
+		break;
+	}
+}
+
+void
+eresize(void)
+{
+	searchr = screen->r;
+	searchr.max.y = searchr.min.y + 2*Padding + 2 + font->height + 2 + Padding;
+	entryr = searchr;
+	entryr.min.x += Padding + stringwidth(font, "Search:") + Padding;
+	entryr.max.x = searchr.max.x - Padding;
+	entryr.min.y += 2*Padding;
+	entryr.max.y -= Padding;
+	entryresize(entry, entryr);
+	viewr = screen->r;
+	viewr.min.y = searchr.max.y + 1;
+	dviewresize(viewr);
+	redraw();
+}
+
+void
+esearch(char *s)
+{
+	Dvec *v;
+
+	v = dictdefine(dict, Dfirstmatch, s);
+	dviewset(v);
+	dviewredraw();
+	flushimage(display, 1);
+}
+
+void
+elink(char *s)
+{
+	entrysettext(entry, s);
+	entryredraw(entry);
+	esearch(s);
+}
+
+void
+initcolors(void)
+{
+	Theme *theme;
+
+	cols = emalloc(sizeof *cols);
+	theme = loadtheme();
+	if(theme == nil){
+		cols->back  = theme->back;
+		cols->text  = theme->text;
+		cols->focus = theme->title;
+		cols->sel   = theme->border;
+		cols->scrl  = theme->border;
+	}else{
+		cols->back  = display->white;
+		cols->text  = display->black;
+		cols->focus = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, DGreygreen);
+		cols->sel   = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, 0xCCCCCCFF);
+		cols->scrl  = allocimage(display, Rect(0, 0, 1, 1), RGBA32, 1, 0x999999FF);
+	}
+}
+
+void
+usage(void)
+{
+	fprint(2, "usage: %s -h <host> [-p <port>]\n", argv0);
+	exits("usage");
+}
+
+void
+threadmain(int argc, char *argv[])
+{
+	enum { Emouse, Eresize, Ekeyboard, Eentry, Elink };
+	Mouse m;
+	Rune k;
+	char *s, *l, *host;
+	int port;
+	Channel *lchan;
+	Alt a[] = {
+		{ nil, &m,  CHANRCV },
+		{ nil, nil, CHANRCV },
+		{ nil, &k,  CHANRCV },
+		{ nil, &s,  CHANRCV },
+		{ nil, &l,  CHANRCV },
+		{ nil, nil, CHANEND },
+	};
+
+	host = nil;
+	port = 2628;
+	ARGBEGIN{
+	case 'h':
+		host = EARGF(usage());
+		break;
+	case 'p':
+		port = atoi(EARGF(usage()));
+		break;
+	}ARGEND;
+	if(host == nil)
+		usage();
+	dict = dictdial(host, port);
+	if(dict == nil)
+		sysfatal("initdict: %r");
+	if(initdraw(nil, nil, argv0) < 0)
+		sysfatal("initdraw: %r");
+	display->locking = 0;
+	if((mc = initmouse(nil, screen)) == nil)
+		sysfatal("initmouse: %r");
+	if((kc = initkeyboard(nil)) == nil)
+		sysfatal("initkeyboard: %r");
+	initcolors();
+	entry = emalloc(sizeof *entry);
+	entryinit(entry, cols);
+	lchan = chancreate(sizeof(char*), 1);
+	dviewinit(lchan, cols);
+	a[Emouse].c = mc->c;
+	a[Eresize].c = mc->resizec;
+	a[Ekeyboard].c = kc->c;
+	a[Eentry].c = entry->c;
+	a[Elink].c = lchan;
+	eresize();
+	entryfocus(entry, 0);
+	for(;;){
+		switch(alt(a)){
+		case Emouse:
+			emouse(m);
+			break;
+		case Eresize:
+			if(getwindow(display, Refnone) < 0)
+				sysfatal("getwindow: %r");
+			eresize();
+			break;
+		case Ekeyboard:
+			ekeyboard(k);
+			break;
+		case Eentry:
+			esearch(s);
+			break;
+		case Elink:
+			elink(l);
+			break;
+		}
+	}
+}
+
binary files /dev/null b/vdict.png differ