shithub: castor9

ref: 41fb4360a4d6ca93f63e9d98c6c12fef463da090
dir: castor9/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"


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

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

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


char* replace_char(char* str, char find, char replace){
    char *current_pos = strchr(str,find);
    while (current_pos){
        *current_pos = replace;
        current_pos = strchr(current_pos,find);
    }
    return str;
}

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';
	
	replace_char(line, '\t', ' ');
	return line;
}

void
set_current_base_url(Url *url)
{
	freeurl(current_base_url);
	current_base_url = url;
}

void
show(Ctx *c)
{
	plinittextview(textp, PACKE|EXPAND, ZP, c->text, texthit);
	pldraw(textp, screen);
	plinitlabel(urlp, PACKN|FILLX, c->url->raw);
	pldraw(urlp, screen);
	message("Castor9");
}

void
plumburl(char *u)
{
	int fd;

	fd = plumbopen("send", OWRITE|OCEXEC);
	if(fd<0)
		return;
	plumbsendtext(fd, "gopher", nil, nil, u);
	close(fd);
}

char *
protocol(char *link)
{
	if(strstr(link, "http://") != nil) {
		return " [WWW]";
	} else if(strstr(link, "https://") != nil) {
		return " [WWW]";
	} else if(strstr(link, "gopher://") != nil) {
		return " [GOPHER]";
	} else if(strstr(link, "finger://") != nil) {
		return " [FINGER]";
	} else if(strstr(link, "mailto:") != nil) {
		return " [MAIL]";
	} else {
		return "";
	}
}

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

void
handle_status(char *status, Response *r)
{
	int code;
	char *meta;
	code = atoi(strtok(status, " "));
	if(code == 0)
		message("STATUS: %s\n", status);
	meta = strtok(NULL, "\n");
	r->status = code;
	r->prompt = cleanup(meta);
}

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

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

	char *preformatted_marker = "```";
	if(strncmp(line, preformatted_marker, strlen(preformatted_marker)) == 0){
		if(preformatted==0){
			preformatted=1;
		}else{
			preformatted=0;
		}
		return;
	}

	while(*base)
	{
		if(preformatted==1)
		{
			plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), PL_HEAD, 0);
			break;
		}
		if((length <= width))
		{
			plrtstr(&c->text, 1000000, 8, 0, font, strdup(cleanup(base)), 0, 0);
			break;
        }
		

		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
render_link(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));
}

Url *
base_url(Url *url)
{
	char *base_url, *path, *ptr;
	
	if(url->path == "/" || url->path == NULL){
		path = "/";
	}else{
		path = estrdup(url->path);
		ptr = strrchr(path, '/');
		if(path[strlen(path)-1] != '/')
			strcpy(ptr, "/");
	}
	base_url = smprint("gemini://%s%s", url->host, path);

	return urlparse(nil, base_url);
}

void
gemini_get(Url *url)
{
	Thumbprint *th;
	TLSconn conn;
	int fd;
	char *line, *port;
	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;

	Hist *h;
	h = malloc(sizeof *h);
	if(h == nil)
		sysfatal("malloc: %r");

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

	message("loading %s...", url->raw);

	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;
	}

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

	fd = tlsClient(fd, &conn);
	th = initThumbprints("/sys/lib/ssl/gemini", nil, "x509");

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

	fprint(fd, "%s\r\n", url->raw);
	Binit(&body, fd, OREAD);

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

	if(r->status == 20){
		c->url = url;
		set_current_base_url(base_url(url));
		
		while((line = Brdstr(&body, '\n', 0)) != nil)
		{
			if (strstr(line, "=>") == NULL) {
				/* Not a link so wrapping text */
				render_text(c, line);
			} else {
				/* a link */
				render_link(c, line);
			}
			free(line);
		}

		Bflush(&body);
		close(fd);

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

		show(c);
	} else if(r->status == 31) {
		Url *redirect = urlparse(nil, r->prompt);
		gemini_get(redirect);
	}
}

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;
	set_current_base_url(base_url(hist->c->url));
	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;
	set_current_base_url(base_url(hist->c->url));
	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 Mexit:
		exits(nil);
		break;
	}
}

void
entryhit(Panel *p, char *t)
{
	USED(p);
	switch(strlen(t)){
	case 0:
		return;
	case 1:
		switch(*t){
		case 'b':
			//backhit(backp, 1);
			break;
		case 'n':
			//nexthit(fwdp, 1);
			break;
		case 'q':
			exits(nil);
			break;
		default:
			message("unknown command %s", t);
			break;
		}
		break;
	default:
		if(strstr(t, "gemini://") == NULL)
			t = smprint("gemini://%s", t);
			
		gemini_get(urlparse(nil, t));
	}
	plinitentry(entryp, PACKN|FILLX, 0, "", entryhit);
	pldraw(root, screen);
}

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 (strstr(link, "gemini://") != nil || strstr(link, "://") != nil){
		next_url = urlparse(nil, link);
	} else {
		/* assuming relative URL */
		if(*link == '/'){
			n = smprint("%s%s", urlparse(current_base_url, link)->raw, estrdup(link)+1);
		}else{
			n = smprint("%s%s", urlparse(current_base_url, link)->raw, estrdup(link));
		}
		next_url = urlparse(nil, n);
	}
	
	if(strcmp(next_url->scheme, "gemini") == 0){
		free(link);
		gemini_get(next_url);
	} else {
		message("%s protocol not supported yet!", link);
		free(link);
	}
}

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;

	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, "Castor!");
	    plplacelabel(statusp, PLACEW);
	    pllabel(p, PACKW, "Go: ");
	    entryp = plentry(p, PACKN|FILLX, 0, "", entryhit);
	  p = plgroup(root, PACKN|FILLX);
	    urlp = pllabel(p, PACKN|FILLX, "");
	    plplacelabel(urlp, 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);
}

Image*
loadicon(Rectangle r, uchar *data, int ndata)
{
	Image *i;
	int n;

	i = allocimage(display, r, RGBA32, 0, DNofill);
	if(i==nil)
		sysfatal("allocimage: %r");
	n = loadimage(i, r, data, ndata);
	if(n<0)
		sysfatal("loadimage: %r");
	return i;
}

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
main(int argc, char *argv[])
{
	Event e;
	Url *url;
	
	if(argc == 2)
		url = urlparse(nil, argv[1]);
	else
		url = urlparse(nil, "gemini://gemini.circumlunar.space/capcom/");

	quotefmtinstall();
	if(initdraw(nil, nil, "gemini")<0)
		sysfatal("initdraw: %r");
	einit(Emouse|Ekeyboard);
	plinit(screen->depth);
	mkpanels();
	gemini_get(url);
	eresized(0);
	for(;;){
		switch(event(&e)){
		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;
		}
	}
}

// //////////////////////////

enum {
	Domlen = 256,
};

static char reserved[] = "%:/?#[]@!$&'()*+,;=";

static int
dhex(char c)
{
	if('0' <= c && c <= '9')
		return c-'0';
	if('a' <= c && c <= 'f')
		return c-'a'+10;
	if('A' <= c && c <= 'F')
		return c-'A'+10;
	return 0;
}

static char*
unescape(char *s, char *spec)
{
	char *r, *w;
	uchar x;

	if(s == nil)
		return s;
	for(r=w=s; x = *r; r++){
		if(x == '%' && isxdigit(r[1]) && isxdigit(r[2])){
			x = (dhex(r[1])<<4)|dhex(r[2]);
			if(spec && strchr(spec, x)){
				*w++ = '%';
				*w++ = toupper(r[1]);
				*w++ = toupper(r[2]);
			}
			else
				*w++ = x;
			r += 2;
			continue;
		}
		*w++ = x;
	}
	*w = 0;
	return s;
}

int
Efmt(Fmt *f)
{
	char *s, *spec;
	Str2 s2;

	s2 = va_arg(f->args, Str2);
	s = s2.s1;
	spec = s2.s2;
	for(; *s; s++)
		if(*s == '%' && isxdigit(s[1]) && isxdigit(s[2])){
			fmtprint(f, "%%%c%c", toupper(s[1]), toupper(s[2]));
			s += 2;
		}
		else if(isalnum(*s) || strchr(".-_~!$&'()*,;=", *s) || strchr(spec, *s))
			fmtprint(f, "%c", *s);
		else
			fmtprint(f, "%%%.2X", *s & 0xff);
	return 0;
}

int
Nfmt(Fmt *f)
{
	char d[Domlen], *s;

	s = va_arg(f->args, char*);
	if(utf2idn(s, d, sizeof(d)) >= 0)
		s = d;
	fmtprint(f, "%s", s);
	return 0;
}

int
Mfmt(Fmt *f)
{
	char *s = va_arg(f->args, char*);
	fmtprint(f, (*s != '[' && strchr(s, ':') != nil)? "[%s]" : "%s", s);
	return 0;
}

int
Ufmt(Fmt *f)
{
	char *s;
	Url *u;

	if((u = va_arg(f->args, Url*)) == nil)
		return fmtprint(f, "nil");
	if(u->scheme)
		fmtprint(f, "%s:", u->scheme);
	if(u->user || u->host)
		fmtprint(f, "//");
	if(u->user){
		fmtprint(f, "%E", (Str2){u->user, ""});
		if(u->pass)
			fmtprint(f, ":%E", (Str2){u->pass, ""});
		fmtprint(f, "@");
	}
	if(u->host){
		fmtprint(f, "%]", u->host);
		if(u->port)
			fmtprint(f, ":%s", u->port);
	}
	if(s = Upath(u))
		fmtprint(f, "%E", (Str2){s, "/:@+"});
	if(u->query)
		fmtprint(f, "?%E", (Str2){u->query, "/:@"});
	if(u->fragment)
		fmtprint(f, "#%E", (Str2){u->fragment, "/:@?+"});
	return 0;
}

char*
Upath(Url *u)
{
	if(u){
		if(u->path)
			return u->path;
		if(u->user || u->host)
			return "/";
	}
	return nil;
}

static char*
remdot(char *s)
{
	char *b, *d, *p;
	int dir, n;

	dir = 1;
	b = d = s;
	if(*s == '/')
		s++;
	for(; s; s = p){
		if(p = strchr(s, '/'))
			*p++ = 0;
		if(*s == '.' && ((s[1] == 0) || (s[1] == '.' && s[2] == 0))){
			if(s[1] == '.')
				while(d > b)
					if(*--d == '/')
						break;
			dir = 1;
			continue;
		} else
			dir = (p != nil);
		if((n = strlen(s)) > 0)
			memmove(d+1, s, n);
		*d++ = '/';
		d += n;
	}
	if(dir)
		*d++ = '/';
	*d = 0;
	return b;
}

static char*
abspath(char *s, char *b)
{
	char *x, *a;

	if(b && *b){
		if(s == nil || *s == 0)
			return estrdup(b);
		if(*s != '/' && (x = strrchr(b, '/'))){
			a = emalloc((x - b) + strlen(s) + 4);
			sprint(a, "%.*s/%s", utfnlen(b, x - b), b, s);
			return remdot(a);
		}
	}
	if(s && *s){
		if(*s != '/')
			return estrdup(s);
		a = emalloc(strlen(s) + 4);
		sprint(a, "%s", s);
		return remdot(a);
	}
	return nil;
}

static void
pstrdup(char **p)
{
	if(p == nil || *p == nil)
		return;
	if(**p == 0){
		*p = nil;
		return;
	}
	*p = estrdup(*p);
}

static char*
mklowcase(char *s)
{
	char *cp;
	Rune r;

	if(s == nil)
		return s;
	cp = s;
	while(*cp != 0){
		chartorune(&r, cp);
		r = tolowerrune(r);
		cp += runetochar(cp, &r);
	}
	return s;
}

static Url *
saneurl(Url *u)
{
	if(u == nil || u->scheme == nil || u->host == nil || Upath(u) == nil){
		freeurl(u);
		return nil;
	}
	if(u->port){
		/* remove default ports */
		switch(atoi(u->port)){
		case 21:	if(!strcmp(u->scheme, "ftp"))	 goto Defport; break;
		case 70:	if(!strcmp(u->scheme, "gopher")) goto Defport; break;
		case 80:	if(!strcmp(u->scheme, "http"))	 goto Defport; break;
		case 443:	if(!strcmp(u->scheme, "https"))	 goto Defport; break;
		case 1965:  if(!strcmp(u->scheme, "gemini")) goto Defport; break;
		default:	if(!strcmp(u->scheme, u->port))	 goto Defport; break;
		Defport:
			free(u->port);
			u->port = nil;
		}
	}
	return u;
}

Url*
urlparse(Url *b, char *s)
{
	char *t, *p, *x, *y;
	Url *u;

	if(s == nil)
		s = "";
	t = nil;
	s = p = estrdup(s);
	u = emalloc(sizeof(*u));

	u->raw = estrdup(s);

	for(; *p; p++){
		if(*p == ':'){
			if(p == s)
				break;
			*p++ = 0;
			u->scheme = s;
			b = nil;
			goto Abs;
		}
		if(!isalpha(*p))
			if((p == s) || ((!isdigit(*p) && strchr("+-.", *p) == nil)))
				break;
	}
	p = s;
	if(b){
		switch(*p){
		case 0:
			memmove(u, b, sizeof(*u));
			goto Out;
		case '#':
			memmove(u, b, sizeof(*u));
			u->fragment = p+1;
			goto Out;
		case '?':
			memmove(u, b, sizeof(*u));
			u->fragment = u->query = nil;
			break;
		case '/':
			if(p[1] == '/'){
				u->scheme = b->scheme;
				b = nil;
				break;
			}
		default:
			memmove(u, b, sizeof(*u));
			u->fragment = u->query = u->path = nil;
			break;
		}
	}
Abs:
	if(x = strchr(p, '#')){
		*x = 0;
		u->fragment = x+1;
	}
	if(x = strchr(p, '?')){
		*x = 0;
		u->query = x+1;
	}
	if(p[0] == '/' && p[1] == '/'){
		p += 2;
		if(x = strchr(p, '/')){
			u->path = t = abspath(x, Upath(b));
			*x = 0;
		}
		if(x = strchr(p, '@')){
			*x = 0;
			if(y = strchr(p, ':')){
				*y = 0;
				u->pass = y+1;
			}
			u->user = p;
			p = x+1;
		}
		if((x = strrchr(p, ']')) == nil)
			x = p;
		if(x = strrchr(x, ':')){
			*x = 0;
			u->port = x+1;
		}
		if(x = strchr(p, '[')){
			p = x+1;
			if(y = strchr(p, ']'))
				*y = 0;
		}
		u->host = p;
	} else {
		u->path = t = abspath(p, Upath(b));
	}
Out:
	pstrdup(&u->scheme);
	pstrdup(&u->user);
	pstrdup(&u->pass);
	pstrdup(&u->host);
	pstrdup(&u->port);
	pstrdup(&u->path);
	pstrdup(&u->query);
	pstrdup(&u->fragment);
	free(s);
	free(t);

	/* the + character encodes space only in query part */
	if(s = u->query)
		while(s = strchr(s, '+'))
			*s++ = ' ';

	if(s = u->host){
		t = emalloc(Domlen);
		if(idn2utf(s, t, Domlen) >= 0){
			u->host = estrdup(t);
			free(s);
		}
		free(t);
	}

	unescape(u->user, nil);
	unescape(u->pass, nil);
	unescape(u->path, reserved);
	unescape(u->query, reserved);
	unescape(u->fragment, reserved);
	mklowcase(u->scheme);
	mklowcase(u->host);
	mklowcase(u->port);

	if((u = saneurl(u)) != nil)
		u->full = smprint("%U", u);

	return u;
}

int
matchurl(Url *u, Url *s)
{
	if(u){
		char *a, *b;

		if(s == nil)
			return 0;
		if(u->scheme && (s->scheme == nil || strcmp(u->scheme, s->scheme)))
			return 0;
		if(u->user && (s->user == nil || strcmp(u->user, s->user)))
			return 0;
		if(u->host && (s->host == nil || strcmp(u->host, s->host)))
			return 0;
		if(u->port && (s->port == nil || strcmp(u->port, s->port)))
			return 0;
		if(a = Upath(u)){
			b = Upath(s);
			if(b == nil || strncmp(a, b, strlen(a)))
				return 0;
		}
	}
	return 1;
}

void
freeurl(Url *u)
{
	if(u == nil)
		return;
	free(u->full);
	free(u->scheme);
	free(u->user);
	free(u->pass);
	free(u->host);
	free(u->port);
	free(u->path);
	free(u->query);
	free(u->fragment);
	free(u->raw);
	free(u);
}

// ///////////////

void *
emalloc(int n)
{
	void *v;
	if((v = malloc(n)) == nil) {
		fprint(2, "out of memory allocating %d\n", n);
		sysfatal("mem");
	}
	setmalloctag(v, getcallerpc(&n));
	memset(v, 0, n);
	return v;
}

char *
estrdup(char *s)
{
	char *t;
	if((t = strdup(s)) == nil) {
		fprint(2, "out of memory in strdup(%.10s)\n", s);
		sysfatal("mem");
	}
	setmalloctag(t, getcallerpc(&t));
	return t;
}