ref: e29ac5e534dc9ec859b5698331fc4d6cf3ef191c
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 gemini_get(Url *u); void gemini_put(Response *r); void texthit(Panel *p, int b, Rtext *t); void addbookmark(void); void showbookmarks(void); void message(char *s, ...); Panel *root; Panel *backp; Panel *fwdp; Panel *entryp; Panel *urlp; Panel *textp; Panel *statusp; Panel *popup; Mouse *mouse; Hist *hist = nil; int preformatted = 0; enum { Mback, Mforward, Msearch, Mbookmarks, Maddbookmark, Mexit, }; char *menu3[] = { "back", "forward", "search", "bookmarks", "add bookmark", "exit", 0 }; Url * current_url(void) { return hist->c->url; } Url * current_base_url(void) { return base_url(current_url()); } char * current_host(void) { Url *base = current_base_url(); return base->host; } 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 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(Url *u) { int fd; fd = plumbopen("send", OWRITE|OCEXEC); if(fd<0) return; char *msg; if(strcmp(u->scheme, "mailto") == 0){ msg = u->path; }else{ msg = u->raw; } 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, "%s\r\n", u->raw); 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 parse_status(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 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(strbeg(line, preformatted_marker) == 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 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)); } 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 gemini_get(Url *url) { int fd; char *line; 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); fd = request(url); fprint(fd, "%s\r\n", url->raw); Binit(&body, fd, OREAD); char *status = Brdstr(&body, '\n', 0); parse_status(status, r); switch(r->status){ case 10: gemini_put(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); message("Castor9"); }else{ while((line = Brdstr(&body, '\n', 0)) != nil){ if(strbeg(line, "=>") == 0){ render_link(c, line); }else{ render_text(c, line); } free(line); } Bflush(&body); close(fd); h->p = hist; h->n = nil; h->c = c; hist = h; show(c); } break; case 30: gemini_get(urlparse(nil, r->meta)); break; case 31: gemini_get(urlparse(nil, 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; } } void gemini_put(Response *r) { char buf[1024]; char *url; strncpy(buf, "", sizeof(buf)-1); if(eenter(r->meta, buf, sizeof(buf), mouse) <= 0){ message("Castor9"); return; } url = smprint("%s?%s", r->url->raw, buf); message("Castor9"); gemini_get(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 = create(getbookmarkspath(), OWRITE, 0600 | DMAPPEND)) < 0) sysfatal("create(bookmarks): %r"); return fd; } void showbookmarks(void) { char *line; Biobuf *bfile; bfile = Bopen(getbookmarkspath(), OREAD); if(bfile==nil){ message("You must bookmark a page first!"); return; } Ctx *c; c = malloc(sizeof *c); if(c==nil) sysfatal("malloc: %r"); c->text = nil; c->url = urlparse(nil, "file://bookmarks"); Hist *h; h = malloc(sizeof *h); if(h == nil) sysfatal("malloc: %r"); plrtstr(&c->text, 1000000, 0, 0, font, strdup(" "), 0, 0); message("loading bookmarks..."); while((line = Brdstr(bfile, '\n', 0)) != nil){ render_link(c, line); free(line); } Bflush(bfile); h->p = hist; h->n = nil; h->c = c; hist = h; show(c); } void addbookmark(void) { int fd; fd = createbookmarks(); fprint(fd, "=> %s\n", hist->c->url->raw); close(fd); message("Bookmark added!"); } void entryhit(Panel *p, char *t) { USED(p); if(strlen(t) == 0) return; if(strbeg(t, "gemini://") != 0) 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(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", current_host()); }else if(*link == '/'){ /* start with a slash so use the base host */ n = smprint("gemini://%s%s", current_host(), estrdup(link)); }else{ /* make an absolute URL of the link */ n = smprint("%s%s", urlparse(current_base_url(), link)->raw, estrdup(link)); } next_url = urlparse(nil, n); } if(strbeg(next_url->raw, "gemini://") == 0){ gemini_get(next_url); }else{ plumburl(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; 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); } 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) if(strbeg(argv[1], "gemini://") != 0){ url = urlparse(nil, smprint("gemini://%s", argv[1])); }else{ 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; } } }