shithub: castor9

ref: 059574a9bdc29c5381e12ed18158f05320ad7cd7
dir: /castor.c/

View raw version
#include <u.h>
#include <libc.h>
#include <libsec.h>
#include <String.h>
#include <regexp.h>
#include <draw.h>
#include <event.h>
#include <keyboard.h>
#include <panel.h>
#include <bio.h>
#include <stdio.h>
#include <ctype.h>
#include <plumb.h>
#include "castor.h"


typedef struct Ctx Ctx;
typedef struct Hist Hist;
typedef struct Response Response;

struct Response
{
	Url *url;
	char *meta;
	int status;
	int fd;
};

struct Ctx
{
	Url *url;
	Rtext *text;
};

struct Hist
{
	Hist *p;
	Hist *n;
	Ctx *c;
};

int request(Url *u);
void geminiget(Url *u);
void geminiput(Response *r);
void texthit(Panel *p, int b, Rtext *t);
void entryhit(Panel *p, char *t);
void addbookmark(void);
void showbookmarks(void);
void message(char *s, ...);
void visit(Url *url);

Panel *root;
Panel *backp;
Panel *fwdp;
Panel *entryp;
Panel *textp;
Panel *statusp;
Panel *popup;
Mouse *mouse;
Hist *hist = nil;
int preformatted = 0;
char *bookmarkspath;
Url *filebase;

enum
{
	Mback,
	Mforward,
	Msearch,
	Mbookmarks,
	Maddbookmark,
	Mexit,
};

char *menu3[] = {
	"back",
	"forward",
	"search",
	"bookmarks",
	"add bookmark",
	"exit",
	0
};

void
resettitle(void)
{
	message("castor9");
}

Url *
currenturl(void)
{
	return hist->c->url;
}

Url *
currentbaseurl(void)
{
	return baseurl(currenturl());
}

char *
currenthost(void)
{
	Url *base = currentbaseurl();
	return base->host;
}

char *
urlstr(Url *url)
{
	return smprint("%U", url);
}

char*
cleanup(char *line)
{
	if(line=="" || line==NULL)
		return line;

	char *src, *dst;
    for(src=dst=line; *src != '\0'; src++){
        *dst = *src;
        if(*dst != '\r' && *dst != '\n') 
			dst++;
    }
    *dst = '\0';
	
	replacechar(line, '\t', ' ');
	return line;
}

void
show(Ctx *c)
{
	plinittextview(textp, PACKE|EXPAND, ZP, c->text, texthit);
	pldraw(textp, screen);
	plinitentry(entryp, PACKN|FILLX, 0, urlstr(c->url), entryhit);
	pldraw(entryp, screen);
	resettitle();
}

void
plumburl(Url *u)
{
	int fd;
	char *msg;

	fd = plumbopen("send", OWRITE|OCEXEC);
	if(fd<0)
		return;

	if(strcmp(u->scheme, "mailto") == 0){
		msg = u->path;
	}else{
		msg = urlstr(u);
	}
	plumbsendtext(fd, "castor9", nil, nil, msg);
	close(fd);
	freeurl(u);
}

void
page(Url *u)
{
	int fd;
	char tmp[32] = "/tmp/castor9XXXXXXXXXXX", *cmd;

	fd = request(u);
	if(fd < 0)
		sysfatal("dial: %r");

	fprint(fd, "%U\r\n", u);
	
	switch(rfork(RFFDG|RFPROC|RFMEM|RFREND|RFNOWAIT|RFNOTEG)){
	case -1:
		fprint(2, "Can't fork!");
		break;
	case 0:
		mktemp(tmp);
		cmd = smprint("tail -n +2 >%s >[2]/dev/null; page -w %s; rm %s", tmp, tmp, tmp);
		dup(fd, 0);
		close(fd);
		execl("/bin/rc", "rc", "-c", cmd, nil);
	}
}

char*
protocol(char *link)
{
	if(strbeg(link, "http://") == 0){
		return " [WWW]";
	}else if(strbeg(link, "https://") == 0){
		return " [WWW]";
	}else if(strbeg(link, "gopher://") == 0){
		return " [GOPHER]";
	}else if(strbeg(link, "finger://") == 0){
		return " [FINGER]";
	}else{
		return "";
	}
}

char*
symbol(char *link)
{
	if(strbeg(link, "http://") == 0){
		return "⇄";
	}else if(strbeg(link, "https://") == 0){
		return "⇄";
	}else if(strbeg(link, "gopher://") == 0){
		return "⇒";
	}else if(strbeg(link, "finger://") == 0){
		return "⇒";
	}else{
		return "→";
	}
}

void
parsestatus(char *status, Response *r)
{
	int code;
	char *meta, *s;

	if(status == nil){
		message("Failed to read response (missing crlf?)", status);
		return;
	}
	if((s = strtok(status, " \t")) != nil){
		code = atoi(s);
		if(code == 0){
			message("Invalid status received: %s", status);
			return;
		}
		meta = strtok(NULL, "\r\n");
		r->status = code;
		r->meta = cleanup(meta);
	}
}

void
rendertext(Ctx *c, char *line)
{
	char *base, *right_margin;
    int length, width;

    length = strlen(strdup(line));
    base = strdup(line);
    width = 80;

	char *preformattedmarker = "```";
	if(strbeg(line, preformattedmarker) == 0){
		if(preformatted==0){
			preformatted=1;
		}else{
			preformatted=0;
		}
		return;
	}

	while(*base){
		/* Preformatted text */
		if(preformatted==1){
			plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), PL_HEAD, 0);
			break;
		}
		/* Headers */
		if(strbeg(line, "#") == 0){
			plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), PL_HEAD, 0);
			break;
        } /* Small lines */		
		if((length <= width)){
			plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), 0, 0);
			break;
        }
		
		/* Wrapping the rest */
		right_margin = base + width;
		while(!isspace(*right_margin)){
			right_margin--;
			if(right_margin == base){
				right_margin += width;
				while(!isspace(*right_margin)){
					if(*right_margin == '\0')
						break;
					right_margin++;
				}
			}
		}
		*right_margin = '\0';
		plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), 0, 0);
		length -= right_margin - base + 1; /* +1 for the space */
		base = right_margin + 1;
	}
}

void
renderlink(Ctx *c, char *line)
{
	char *copy = strdup(cleanup(line + 2)); /* bypass => */
	char *link = strtok(copy, " ");
	char *rest = strtok(NULL, "\0");
	char *label;

	if(rest != NULL){
		while(isspace(*rest))
 			rest++;

		label = smprint("%s %s%s", symbol(link), rest, protocol(link));
	}else{
		label = smprint("%s %s%s", symbol(link), link, protocol(link));
	}

	plrtstr(&c->text, 1000000, 8, 0, font, strdup(label), PL_HOT, estrdup(link));
}

int
request(Url *url)
{
	Thumbprint *th;
	TLSconn conn;
	int fd;
	char *port;

	if(url->port == NULL){
		port = "1965";
	}else{
		port = url->port;
	}
	char *naddr = netmkaddr(url->host, "tcp", port);
	fd = dial(naddr, 0, 0, 0);
	if(fd < 0){
		message("unable to connect to %s:%s: %r", url->host, url->port);
		return -1;
	}

	memset(&conn, 0, sizeof(conn));
	conn.serverName = url->host;

	fd = tlsClient(fd, &conn);
	if(fd < 0){
		message("tls: %r");
		return -1;
	}

	th = initThumbprints("/sys/lib/ssl/gemini", nil, "x509");

	if(th != nil){
		okCertificate(conn.cert, conn.certlen, th);
		freeThumbprints(th);
		free(conn.cert);
	}

	return fd;
}

void
geminishow(Ctx *c, Biobuf *body)
{
	char *line;
	Hist *h;
	
	h = malloc(sizeof *h);
	if(h == nil)
		sysfatal("malloc: %r");

	while((line = Brdstr(body, '\n', 0)) != nil){
		if(strbeg(line, "=>") == 0){
			renderlink(c, line);					
		}else{
			rendertext(c, line);
		}
		free(line);
	}

	Bflush(body);

	h->p = hist;
	h->n = nil;
	h->c = c;
	hist = h;

	show(c);
}

void
geminiget(Url *url)
{
	int fd;
	Biobuf body;

	Ctx *c;
	c = malloc(sizeof *c);
	if(c==nil)
		sysfatal("malloc: %r");
	c->text = nil;

	Response *r;
	r = malloc(sizeof *r);
	if(r == nil)
		sysfatal("malloc: %r");
	r->url = url;

	plrtstr(&c->text, 1000000, 0, 0, font, strdup(" "), 0, 0);

	message("loading %s...", urlstr(url));

	fd = request(url);
	fprint(fd, "%U\r\n", url);
	Binit(&body, fd, OREAD);

	char *status = Brdstr(&body, '\n', 0);
	parsestatus(status, r);

	switch(r->status){
	case 10:
		geminiput(r);
		break;
	case 11:
		message("Sensitive input! %s", r->meta);
		break;
	case 20:
		c->url = url;

		if(r->meta != NULL && strbeg(r->meta, "text/") != 0){
			Bflush(&body);
			close(fd);

			page(url);
			resettitle();
		}else{
			geminishow(c, &body);
		}
		break;
	case 30:
		geminiget(urlparse(url, r->meta));	
		break;
	case 31:
		geminiget(urlparse(url, r->meta));
		break;
	case 40:
		message("Temporary failure, please try again later!");
		break;
	case 41:
		message("Server unavailable!");
		break;
	case 42:
		message("CGI error!");
		break;
	case 43:
		message("Proxy error!");
		break;
	case 44:
		message("Slow down!");
		break;
	case 50:
		message("Permanent failure!");
		break;
	case 51:
		message("Not found!");
		break;
	case 52:
		message("Gone!");
		break;
	case 53:
		message("Proxy request refused!");
		break;
	case 59:
		message("Bad request!");
		break;
	case 60:
		message("Client certificate required!");
		break;
	case 61:
		message("Certificate not authorised!");
		break;
	case 62:
		message("Certificate not valid!");
		break;
	//default:
	//	message("Unknown status code %d!", status);
	//	break;
	}
	close(fd);
}

void
geminiput(Response *r)
{
	char buf[1024];
	char *url;

	resettitle();
	strncpy(buf, "", sizeof(buf)-1);
	if(eenter(r->meta, buf, sizeof(buf), mouse) <= 0)
		return;
	
	url = smprint("%U?%s", r->url, buf);
	geminiget(urlparse(nil, url));
}

void
search(void)
{
	static char last[256];
	char buf[256];
	Reprog *re;
	Rtext *tp;
	int yoff;

	for(;;){
		if(hist == nil || hist->c == nil || hist->c->text == nil)
			return;
		strncpy(buf, last, sizeof(buf)-1);
		if(eenter("Search for", buf, sizeof(buf), mouse) <= 0)
			return;
		strncpy(last, buf, sizeof(buf)-1);
		re = regcompnl(buf);
		if(re == nil){
			message("%r");
			continue;
		}
		for(tp=hist->c->text;tp;tp=tp->next)
			if(tp->flags & PL_SEL)
				break;
		if(tp == nil)
			tp = hist->c->text;
		else {
			tp->flags &= ~PL_SEL;
			tp = tp->next;
		}
		while(tp != nil){
			tp->flags &= ~PL_SEL;
			if(tp->text && *tp->text)
			if(regexec(re, tp->text, nil, 0)){
				tp->flags |= PL_SEL;
				plsetpostextview(textp, tp->topy);
				break;
			}
			tp = tp->next;
		}
		free(re);
		yoff = plgetpostextview(textp);
		plinittextview(textp, PACKE|EXPAND, ZP, hist->c->text, texthit);
		plsetpostextview(textp, yoff);
		pldraw(textp, screen);
	}
}

void
backhit(Panel *p, int b)
{
	USED(p);
	if(b!=1)
		return;
	if(hist==nil || hist->p==nil)
		return;
	hist->p->n = hist;
	hist = hist->p;
	show(hist->c);
}

void
nexthit(Panel *p, int b)
{
	USED(p);
	if(b!=1)
		return;
	if(hist==nil || hist->n==nil)
		return;
	hist = hist->n;
	show(hist->c);
}

void
menuhit(int button, int item)
{
	USED(button);

	switch(item){
	case Mback:
		backhit(backp, 1);
		break;
	case Mforward:
		nexthit(fwdp, 1);
		break;
	case Msearch:
		search();
		break;
	case Mbookmarks:
		showbookmarks();
		break;
	case Maddbookmark:
		addbookmark();
		break;
	case Mexit:
		exits(nil);
		break;
	}
}

char*
getbookmarkspath(void)
{
	char *home, *bpath;
	home = getenv("home");
	if(home==0)
		sysfatal("getenv(home): %r");

	bpath = smprint("%s/lib/castorbookmarks", home);
	return bpath;
}

int
createbookmarks(void)
{
	int fd;

	if((fd = open(bookmarkspath, OWRITE)) < 0)
		sysfatal("open(bookmarks): %r");
	if(seek(fd, 0, 2)<0)
		sysfatal("seek(bookmarks): %r");	
		
	return fd;
}

void
showbookmarks(void)
{
	visit(urlparse(filebase, bookmarkspath));
}

void
addbookmark(void)
{
	int fd;
	fd = createbookmarks();
	fprint(fd, "=> %U\n", hist->c->url);
	close(fd);
	message("Bookmark added!");
}

void
entryhit(Panel *p, char *t)
{
	USED(p);
	if(strlen(t) == 0)
		return;

	if(strchr(t, ':') == 0)
		t = smprint("gemini://%s", t);
			
	visit(urlparse(currentbaseurl(), t));
}

void
texthit(Panel *p, int b, Rtext *rt)
{
	char *n;
	Url *next_url;
	char *link = rt->user;

	USED(p);
	if(b!=1)
		return;
	if(link==nil)
		return;

	if(strbeg(link, "gemini://") == 0){							/* gemini absolute */
		next_url = urlparse(nil, link);
	}else if(strstr(link, "://") != 0){							/* other protocol absolute */
		next_url = urlparse(nil, link);
	}else if(strbeg(link, "//") == 0){							/* schemeless so gemini */
		next_url = urlparse(nil, smprint("gemini:%s", link));
	}else if(strbeg(link, "mailto:") == 0){						/* mailto: */
		next_url = urlparse(nil, link);
	}else{
		/* assuming relative URL */
		if(strcmp(link, "/") == 0){
			/* no slash, must be a hostname */
			n = smprint("gemini://%s", currenthost());
		}else if(*link == '/'){
			/* start with a slash so use the base host */
			n = smprint("gemini://%s%s", currenthost(), estrdup(link));
		}else{
			/* make an absolute URL of the link */
			n = urlstr(urlparse(currentbaseurl(), link));
		}
		next_url = urlparse(nil, n);
	}
	
	visit(next_url);
}

void 
message(char *s, ...)
{
	static char buf[1024];
	char *out;
	va_list args;

	va_start(args, s);
	out = buf + vsnprint(buf, sizeof(buf), s, args);
	va_end(args);
	*out='\0';
	plinitlabel(statusp, PACKN|FILLX, buf);
	pldraw(statusp, screen);
	flushimage(display, 1);
}

void
mkpanels(void)
{
	Panel *p, *ybar, *xbar, *m, *sp;

	m = plmenu(0, 0, menu3, PACKN|FILLX, menuhit);
	root = plpopup(0, EXPAND, 0, 0, m);
	p = plgroup(root, PACKN|FILLX);
		statusp = pllabel(p, PACKN|FILLX, "castor9");
		plplacelabel(statusp, PLACEW);
		pllabel(p, PACKW, "Go: ");
		entryp = plentry(p, PACKN|FILLX, 0, "", entryhit);
	p = plgroup(root, PACKN|FILLX);
		sp= pllabel(p, PACKN|FILLX, "");
		plplacelabel(sp, PLACEW);
	p = plgroup(root, PACKN|EXPAND);
		ybar = plscrollbar(p, PACKW|USERFL);
		xbar = plscrollbar(p, IGNORE);
		textp = pltextview(p, PACKE|EXPAND, ZP, nil, nil);
		plscroll(textp, xbar, ybar);
	plgrabkb(entryp);
}

void
eresized(int new)
{
	if(new && getwindow(display, Refnone)<0)
		sysfatal("cannot reattach: %r");
	plpack(root, screen->r);
	pldraw(root, screen);
}

void 
scrolltext(int dy, int whence)
{
	Scroll s;

	s = plgetscroll(textp);
	switch(whence){
	case 0:
		s.pos.y = dy;
		break;
	case 1:
		s.pos.y += dy;
		break;
	case 2:
		s.pos.y = s.size.y+dy;
		break;
	}
	if(s.pos.y > s.size.y)
		s.pos.y = s.size.y;
	if(s.pos.y < 0)
		s.pos.y = 0;
	plsetscroll(textp, s);
	/* BUG: there is a redraw issue when scrolling
	   This fixes the issue albeit not properly */
	pldraw(textp, screen);
}

void
visit(Url *url)
{
	if(strcmp(url->scheme, "gemini") == 0){
		geminiget(url);
	}else if(strcmp(url->scheme, "file") == 0){
		Ctx *c;
		Biobuf* b;
		
		c = malloc(sizeof *c);
		if(c == nil)
			sysfatal("malloc: %r");
		c->text = nil;
		c->url = url;
		plrtstr(&c->text, 1000000, 0, 0, font, strdup(" "), 0, 0);
		b = Bopen(url->path, OREAD);
		if(b == nil)
			sysfatal("open: %r");
		geminishow(c, b);
	}else{
		plumburl(url);
	}
}

void
main(int argc, char *argv[])
{
	Event e;
	Url *url;
	enum { Eplumb = 128 };
	Plumbmsg *pm;
	char buf[256];
	
	filebase = urlparse(nil, smprint("file://%s/%s/", getenv("sysname"),getwd(buf, sizeof buf)));
	argv0 = argv[0];
	if(argc == 2){
		if(strchr(argv[1], ':') || access(argv[1], AEXIST) == 0)
			url = urlparse(filebase, argv[1]);
		else
			url = urlparse(nil, smprint("gemini://%s", argv[1]));
	}else
		url = urlparse(nil, "gemini://gemini.circumlunar.space/capcom/");
	if(url == nil)
		sysfatal("bad url");
	quotefmtinstall();
	fmtinstall('U', Ufmt);
	fmtinstall('N', Nfmt);
	fmtinstall(']', Mfmt);
	fmtinstall('E', Efmt);
	fmtinstall('[', encodefmt);
	fmtinstall('H', encodefmt);
	bookmarkspath = getbookmarkspath();

	if(initdraw(nil, nil, "gemini")<0)
		sysfatal("initdraw: %r");
	einit(Emouse|Ekeyboard);
	plinit(screen->depth);
	mkpanels();
	visit(url);
	eresized(0);
	eplumb(Eplumb, "gemini");
	for(;;){
		switch(event(&e)){
		case Eplumb:
			pm = e.v;
			if(pm->ndata > 0){
				visit(urlparse(nil, pm->data));
			}
			plumbfree(pm);
			break;
		case Ekeyboard:
			switch(e.kbdc){
			default:
				plgrabkb(entryp);
				plkeyboard(e.kbdc);
				break;
			case Khome:
				scrolltext(0, 0);
				break;
			case Kup:
				scrolltext(-textp->size.y/4, 1);
				break;
			case Kpgup:
				scrolltext(-textp->size.y/2, 1);
				break;
			case Kdown:
				scrolltext(textp->size.y/4, 1);
				break;
			case Kpgdown:
				scrolltext(textp->size.y/2, 1);
				break;
			case Kend:
				scrolltext(-textp->size.y, 2);
				break;
			case Kdel:
				exits(nil);
				break;
			}
			break;
		case Emouse:
			mouse = &e.mouse;
			if(mouse->buttons & (8|16) && ptinrect(mouse->xy, textp->r)){
				if(mouse->buttons & 8)
					scrolltext(textp->r.min.y - mouse->xy.y, 1);
				else
					scrolltext(mouse->xy.y - textp->r.min.y, 1);
				break;
			}
			plmouse(root, mouse);
			/* BUG: there is a redraw issue when scrolling
			   This fixes the issue albeit not properly */
			//pldraw(textp, screen);
			break;
		}
	}
}