ref: 059574a9bdc29c5381e12ed18158f05320ad7cd7
dir: /castor.c/
#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;
}
}
}