Commit Diff


commit - /dev/null
commit + 5e11c00c40910bea9deb4b2199868f6dde63798e
blob - /dev/null
blob + 2af19fe67c4e2e1d0c6b2c91d6cd44191cfe1a44 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1,15 @@
+TAGS
+.deps
+Makefile
+Makefile.in
+aclocal.m4
+autom4te.cache
+compile
+config.*
+configure
+depcomp
+install-sh
+missing
+stamp-h1
+*.o
+telescope
blob - /dev/null
blob + a47df7ecbb01ead9247b223e059a1ec95dbbefb5 (mode 644)
--- /dev/null
+++ Makefile.am
@@ -0,0 +1,6 @@
+bin_PROGRAMS =		telescope
+telescope_SOURCES =	telescope.c gemini.c gemtext.c url.c util.c about.c ui.c
+
+dist_doc_DATA =		README.md
+
+AM_LDFLAGS =		${IMSG_LDFLAGS}
blob - /dev/null
blob + 03d13c1fc94f2b6c495979a17bcb8d3629e35af5 (mode 644)
--- /dev/null
+++ README.md
@@ -0,0 +1,3 @@
+# Telescope
+
+Telescope is a w3m-like browser for Gemini.
blob - /dev/null
blob + a9e8f0bb56cd5226b87d4bef667d9269adf52a6e (mode 644)
--- /dev/null
+++ about.c
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <telescope.h>
+
+#define ASCII_ART							\
+	"```An Ascii art of the word \"Telescope\"\n"			\
+	" _______         __\n"						\
+	"|_     _|.-----.|  |.-----.-----.----.-----.-----.-----.\n"	\
+	"  |   |  |  -__||  ||  -__|__ --|  __|  _  |  _  |  -__|\n"	\
+	"  |___|  |_____||__||_____|_____|____|_____|   __|_____|\n"	\
+	"                                           |__|\n"		\
+	"```\n"
+
+const char *about_new =
+	ASCII_ART
+	"\n"
+	"Version: " VERSION "\n"
+	"Bug reports to: " PACKAGE_BUGREPORT "\n"
+	"=> " PACKAGE_URL " Telescope Gemini site: " PACKAGE_URL "\n"
+	"\n"
+	"*test\n"
+	">quote\n"
+	;
blob - /dev/null
blob + 628c689871163f5bcb8d7d8e26aec2a9d144692f (mode 755)
--- /dev/null
+++ autogen.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec autoreconf --install
blob - /dev/null
blob + 8fd6b69aad5ec4b5c2f16a83d35000248847ade6 (mode 644)
--- /dev/null
+++ configure.ac
@@ -0,0 +1,30 @@
+AC_INIT([telescope], [0.1], [telescope@omarpolo.com], [telescope], [gemini://telescope.omarpolo.com])
+AM_INIT_AUTOMAKE([-Wall foreign])
+AC_PROG_CC
+
+PKG_PROG_PKG_CONFIG
+
+dnl AX_WITH_CURSES
+dnl if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then
+dnl 	AC_MSG_ERROR([requires either NcursesW or Ncurses library])
+dnl fi
+
+AC_CHECK_LIB(ncursesw, initscr, [],
+	AC_CHECK_LIB(ncurses, initscr, [],
+		AC_MSG_ERROR([requires either ncursesw or ncurses library])))
+
+AC_CHECK_LIB(tls, tls_init, [],
+	AC_MSG_ERROR([requires libtls]))
+
+AC_CHECK_LIB(event, event_init, [],
+	AC_MSG_ERROR([requires libevent]))
+
+IMSG_LDFLAGS=-lutil
+AC_SUBST([IMSG_LDFLAGS])
+
+AC_CONFIG_HEADERS([config.h])
+AC_CONFIG_FILES([
+	Makefile
+])
+
+AC_OUTPUT
blob - /dev/null
blob + f510a44129c6f2446a709f1546452e51701bbe85 (mode 644)
--- /dev/null
+++ gemini.c
@@ -0,0 +1,530 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * TODO:
+ *  - move the various
+ *	imsg_compose(...);
+ *	imsg_flush(...);
+ *    to something more asynchronous
+ */
+
+#include <telescope.h>
+
+#include <sys/types.h>
+#include <sys/socket.h>
+
+#include <netinet/in.h>
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <event.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <tls.h>
+#include <unistd.h>
+
+static struct tls_config	*tlsconf;
+static struct imsgbuf		*ibuf;
+
+struct req;
+
+static void		 die(void) __attribute__((__noreturn__));
+static char		*xasprintf(const char*, ...);
+static int 		 conn_towards(struct url*, char**);
+
+static void		 close_with_err(struct req*, const char *err);
+static struct req	*req_by_id(uint32_t);
+
+static void		 do_handshake(int, short, void*);
+static void		 write_request(int, short, void*);
+static void		 read_reply(int, short, void*);
+static void		 parse_reply(struct req*);
+static void		 copy_body(int, short, void*);
+
+static void		 check_special_page(struct req*, const char*);
+
+static void		 handle_get(struct imsg*, size_t);
+static void		 handle_cert_status(struct imsg*, size_t);
+static void		 handle_stop(struct imsg*, size_t);
+static void		 handle_quit(struct imsg*, size_t);
+
+static imsg_handlerfn *handlers[] = {
+	[IMSG_GET] = handle_get,
+	[IMSG_CERT_STATUS] = handle_cert_status,
+	[IMSG_STOP] = handle_stop,
+	[IMSG_QUIT] = handle_quit,
+};
+
+typedef void (*statefn)(int, short, void*);
+
+TAILQ_HEAD(, req) reqhead;
+/* a pending request */
+struct req {
+	struct event		 ev;
+	struct url		 url;
+	uint32_t		 id;
+	int			 fd;
+	struct tls		*ctx;
+	char			 buf[1024];
+	size_t			 off;
+	TAILQ_ENTRY(req)	 reqs;
+};
+
+static inline void
+yield_r(struct req *req, statefn fn, struct timeval *tv)
+{
+	event_once(req->fd, EV_READ, fn, req, tv);
+}
+
+static inline void
+yield_w(struct req *req, statefn fn, struct timeval *tv)
+{
+	event_once(req->fd, EV_WRITE, fn, req, tv);
+}
+
+static inline void
+advance_buf(struct req *req, size_t len)
+{
+	assert(len <= req->off);
+
+	req->off -= len;
+	memmove(req->buf, req->buf + len, req->off);
+}
+
+static void __attribute__((__noreturn__))
+die(void)
+{
+	abort(); 		/* TODO */
+}
+
+static char *
+xasprintf(const char *fmt, ...)
+{
+	va_list ap;
+	char *s;
+
+	va_start(ap, fmt);
+	if (vasprintf(&s, fmt, ap) == -1)
+		s = NULL;
+	va_end(ap);
+
+	return s;
+}
+
+static int
+conn_towards(struct url *url, char **err)
+{
+	struct addrinfo	 hints, *servinfo, *p;
+	int		 status, sock;
+	const char	*proto = "1965";
+
+	*err = NULL;
+
+	if (*url->port != '\0')
+		proto = url->port;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+
+	if ((status = getaddrinfo(url->host, proto, &hints, &servinfo))) {
+		*err = xasprintf("failed to resolve %s: %s",
+		    url->host, gai_strerror(status));
+		return -1;
+	}
+
+	sock = -1;
+	for (p = servinfo; p != NULL; p = p->ai_next) {
+		if ((sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1)
+			continue;
+		if (connect(sock, p->ai_addr, p->ai_addrlen) != -1)
+			break;
+		close(sock);
+	}
+
+	if (sock == -1)
+		*err = xasprintf("couldn't connect to %s", url->host);
+	else
+		mark_nonblock(sock);
+
+	freeaddrinfo(servinfo);
+	return sock;
+}
+
+static struct req *
+req_by_id(uint32_t id)
+{
+	struct req *r;
+
+	TAILQ_FOREACH(r, &reqhead, reqs) {
+		if (r->id == id)
+			return r;
+	}
+
+	die();
+}
+
+static void
+close_conn(int fd, short ev, void *d)
+{
+	struct req	*req = d;
+
+	if (req->ctx != NULL) {
+		switch (tls_close(req->ctx)) {
+		case TLS_WANT_POLLIN:
+			yield_r(req, close_conn, NULL);
+			return;
+		case TLS_WANT_POLLOUT:
+			yield_w(req, close_conn, NULL);
+			return;
+		}
+
+		tls_free(req->ctx);
+	}
+
+	TAILQ_REMOVE(&reqhead, req, reqs);
+	if (req->fd != -1)
+		close(req->fd);
+	free(req);
+}
+
+static void
+close_with_err(struct req *req, const char *err)
+{
+	imsg_compose(ibuf, IMSG_ERR, req->id, 0, -1, err, strlen(err)+1);
+	imsg_flush(ibuf);
+	close_conn(0, 0, req);
+}
+
+static void
+do_handshake(int fd, short ev, void *d)
+{
+	struct req	*req = d;
+	const char	*hash;
+
+	switch (tls_handshake(req->ctx)) {
+	case TLS_WANT_POLLIN:
+		yield_r(req, do_handshake, NULL);
+		return;
+	case TLS_WANT_POLLOUT:
+		yield_w(req, do_handshake, NULL);
+		return;
+	}
+
+	hash = tls_peer_cert_hash(req->ctx);
+	imsg_compose(ibuf, IMSG_CHECK_CERT, req->id, 0, -1, hash, strlen(hash)+1);
+	imsg_flush(ibuf);
+}
+
+static void
+write_request(int fd, short ev, void *d)
+{
+	struct req	*req = d;
+	ssize_t		 r;
+	size_t		 len;
+	char		 buf[1024], *err;
+
+	strlcpy(buf, "gemini://", sizeof(buf));
+	strlcat(buf, req->url.host, sizeof(buf));
+	strlcat(buf, "/", sizeof(buf));
+	strlcat(buf, req->url.path, sizeof(buf));
+
+	if (req->url.query[0] != '\0') {
+		strlcat(buf, "?", sizeof(buf));
+		strlcat(buf, req->url.query, sizeof(buf));
+	}
+
+	len = strlcat(buf, "\r\n", sizeof(buf));
+
+	assert(len <= sizeof(buf));
+
+	switch (r = tls_write(req->ctx, buf, len)) {
+	case -1:
+		err = xasprintf("tls_write: %s", tls_error(req->ctx));
+		close_with_err(req, err);
+		free(err);
+		break;
+	case TLS_WANT_POLLIN:
+		yield_r(req, write_request, NULL);
+		break;
+	case TLS_WANT_POLLOUT:
+		yield_w(req, write_request, NULL);
+		break;
+	default:
+		/* assume r == len */
+		(void)r;
+		yield_r(req, read_reply, NULL);
+		break;
+	}
+}
+
+static void
+read_reply(int fd, short ev, void *d)
+{
+	struct req	*req = d;
+	size_t		 len;
+	ssize_t		 r;
+	char		*buf, *e;
+
+	buf = req->buf + req->off;
+	len = sizeof(req->buf) - req->off;
+
+	switch (r = tls_read(req->ctx, buf, len)) {
+	case -1:
+		e = xasprintf("tls_read: %s", tls_error(req->ctx));
+		close_with_err(req, e);
+		free(e);
+		break;
+	case TLS_WANT_POLLIN:
+		yield_r(req, read_reply, NULL);
+		break;
+	case TLS_WANT_POLLOUT:
+		yield_w(req, read_reply, NULL);
+		break;
+	default:
+		req->off += r;
+
+		/* TODO: really watch for \r\n not \n alone */
+		if ((e = telescope_strnchr(req->buf, '\n', req->off)) != NULL)
+			parse_reply(req);
+		else if (req->off == sizeof(req->buf))
+			close_with_err(req, "invalid response");
+		else
+			yield_r(req, read_reply, NULL);
+		break;
+	}
+}
+
+static void
+parse_reply(struct req *req)
+{
+	int	 code;
+	char	*e;
+	size_t	 len;
+
+	if (req->off < 4)
+		goto err;
+
+	if (!isdigit(req->buf[0]) || !isdigit(req->buf[1]))
+		goto err;
+
+	code = (req->buf[0] - '0')*10 + (req->buf[1] - '0');
+
+	if (!isspace(req->buf[2]))
+		goto err;
+
+	advance_buf(req, 3);
+	if ((e = telescope_strnchr(req->buf, '\r', req->off)) == NULL)
+		goto err;
+
+	*e = '\0';
+	e++;
+	len = e - req->buf;
+	imsg_compose(ibuf, IMSG_GOT_CODE, req->id, 0, -1, &code, sizeof(code));
+	imsg_compose(ibuf, IMSG_GOT_META, req->id, 0, -1,
+	    req->buf, len);
+	imsg_flush(ibuf);
+
+	yield_r(req, copy_body, NULL);
+	return;
+
+err:
+	close_with_err(req, "malformed request");
+}
+
+static void
+copy_body(int fd, short ev, void *d)
+{
+	struct req	*req = d;
+	char		 buf[BUFSIZ];
+	ssize_t		 r;
+
+	for (;;) {
+		switch (r = tls_read(req->ctx, buf, sizeof(buf))) {
+		case TLS_WANT_POLLIN:
+			yield_r(req, copy_body, NULL);
+			return;
+		case TLS_WANT_POLLOUT:
+			yield_w(req, copy_body, NULL);
+			return;
+		case -1:
+		case 0:
+			imsg_compose(ibuf, IMSG_EOF, req->id, 0, -1, NULL, 0);
+			imsg_flush(ibuf);
+			close_conn(0, 0, req);
+			return;
+		default:
+			imsg_compose(ibuf, IMSG_BUF, req->id, 0, -1, buf, r);
+			imsg_flush(ibuf);
+			break;
+		}
+	}
+}
+
+static int
+serve_special_page(struct req *req, const char *url)
+{
+	int		 code = 20;
+	const char	*meta = "text/gemini";
+
+	if (strcmp(url, "about:new"))
+		return 0;
+
+	imsg_compose(ibuf, IMSG_GOT_CODE, req->id, 0, -1, &code, sizeof(code));
+	imsg_compose(ibuf, IMSG_GOT_META, req->id, 0, -1, meta, strlen(meta)+1);
+	imsg_compose(ibuf, IMSG_BUF, req->id, 0, -1, about_new, strlen(about_new));
+	imsg_compose(ibuf, IMSG_EOF, req->id, 0, -1, NULL, 0);
+	imsg_flush(ibuf);
+
+	/* don't close_page here, since req is not added to requests
+	 * queue */
+	free(req);
+	return 1;
+}
+
+static void
+handle_get(struct imsg *imsg, size_t datalen)
+{
+	struct req	*req;
+	const char	*e;
+	char		*data, *err = NULL;
+
+	data = imsg->data;
+
+	if (data[datalen-1] != '\0')
+		die();
+
+	if ((req = calloc(1, sizeof(*req))) == NULL)
+		die();
+
+	req->id = imsg->hdr.peerid;
+
+	if (serve_special_page(req, data))
+		return;
+
+        if (!url_parse(imsg->data, &req->url, &e)) {
+		fprintf(stderr, "failed to parse url: %s\n", e);
+		close_with_err(req, e);
+		return;
+	}
+
+	if ((req->fd = conn_towards(&req->url, &err)) == -1)
+		goto err;
+	if ((req->ctx = tls_client()) == NULL)
+		goto err;
+	if (tls_configure(req->ctx, tlsconf) == -1) {
+		err = xasprintf("tls_configure: %s", tls_error(req->ctx));
+		goto err;
+	}
+	if (tls_connect_socket(req->ctx, req->fd, req->url.host) == -1) {
+		err = xasprintf("tls_connect_socket: %s", tls_error(req->ctx));
+		goto err;
+	}
+
+	TAILQ_INSERT_HEAD(&reqhead, req, reqs);
+	yield_w(req, do_handshake, NULL);
+	return;
+
+err:
+        close_with_err(req, err);
+	free(err);
+}
+
+static void
+handle_cert_status(struct imsg *imsg, size_t datalen)
+{
+	struct req	*req;
+	int		 is_ok;
+
+	req = req_by_id(imsg->hdr.peerid);
+
+	if (datalen < sizeof(is_ok))
+		die();
+	memcpy(&is_ok, imsg->data, sizeof(is_ok));
+
+	if (is_ok)
+		yield_w(req, write_request, NULL);
+	else
+		close_conn(0, 0, req);
+}
+
+static void
+handle_stop(struct imsg *imsg, size_t datalen)
+{
+	struct req	*req;
+
+	req = req_by_id(imsg->hdr.peerid);
+	close_conn(0, 0, req);
+}
+
+static void
+handle_quit(struct imsg *imsg, size_t datalen)
+{
+	event_loopbreak();
+}
+
+static void
+dispatch_imsg(int fd, short ev, void *d)
+{
+	struct imsgbuf	*ibuf = d;
+	struct imsg	 imsg;
+	ssize_t		 n;
+	size_t		 datalen;
+
+	if ((n = imsg_read(ibuf)) == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			return;
+		die();
+	}
+
+	if (n == 0)
+		die();
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			die();
+		if (n == 0)
+			return;
+		datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+		handlers[imsg.hdr.type](&imsg, datalen);
+		imsg_free(&imsg);
+	}
+}
+
+int
+client_main(struct imsgbuf *b)
+{
+	ibuf = b;
+
+	TAILQ_INIT(&reqhead);
+
+	if ((tlsconf = tls_config_new()) == NULL)
+		die();
+	tls_config_insecure_noverifycert(tlsconf);
+
+	event_init();
+
+	event_set(&imsgev, ibuf->fd, EV_READ | EV_PERSIST, dispatch_imsg, ibuf);
+	event_add(&imsgev, NULL);
+
+	event_dispatch();
+	return 0;
+}
blob - /dev/null
blob + b74c5ce7f4397b3d7d60c58c9180459d235feb23 (mode 644)
--- /dev/null
+++ gemtext.c
@@ -0,0 +1,406 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * A streaming gemtext parser.
+ *
+ * TODO:
+ *  - handle NULs
+ *  - UTF8
+ */
+
+#include <telescope.h>
+
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.h>
+
+static int	gemtext_parse(struct parser*, const char*, size_t);
+static void	gemtext_free(struct parser*);
+
+static int	parse_text(struct parser*, enum line_type, const char*, size_t);
+static int	parse_link(struct parser*, enum line_type, const char*, size_t);
+static int	parse_title(struct parser*, enum line_type, const char*, size_t);
+static int	parse_item(struct parser*, enum line_type, const char*, size_t);
+static int	parse_quote(struct parser*, enum line_type, const char*, size_t);
+static int	parse_pre_start(struct parser*, enum line_type, const char*, size_t);
+static int	parse_pre_cnt(struct parser*, enum line_type, const char*, size_t);
+static int	parse_pre_end(struct parser*, enum line_type, const char*, size_t);
+
+typedef int (parselinefn)(struct parser*, enum line_type, const char*, size_t);
+
+static parselinefn *parsers[] = {
+	parse_text,		/* LINE_TEXT */
+	parse_link,		/* LINE_LINK */
+	parse_title,		/* LINE_TITLE_1 */
+	parse_title,		/* LINE_TITLE_2 */
+	parse_title,		/* LINE_TITLE_3 */
+	parse_item,		/* LINE_ITEM */
+	parse_quote,		/* LINE_QUOTE */
+	parse_pre_start,	/* LINE_PRE_START */
+	parse_pre_cnt,		/* LINE_PRE_CONTENT */
+	parse_pre_end,		/* LINE_PRE_END */
+};
+
+void
+gemtext_initparser(struct parser *p)
+{
+	memset(p, 0, sizeof(*p));
+
+	p->parse = &gemtext_parse;
+	p->free  = &gemtext_free;
+}
+
+static inline int
+emit_line(struct parser *p, enum line_type type, char *line, char *alt)
+{
+	struct line *l;
+
+	if ((l = calloc(1, sizeof(*l))) == NULL)
+		return 0;
+
+	l->type = type;
+	l->line = line;
+	l->alt = alt;
+
+	if (TAILQ_EMPTY(&p->head))
+		TAILQ_INSERT_HEAD(&p->head, l, lines);
+	else
+		TAILQ_INSERT_TAIL(&p->head, l, lines);
+
+	return 1;
+}
+
+static int
+parse_text(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+	memcpy(l, buf, len);
+	return emit_line(p, t, l, NULL);
+}
+
+static int
+parse_link(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l, *u;
+	const char *url_start;
+
+	if (len <= 2)
+		return emit_line(p, t, NULL, NULL);
+	buf += 2;
+	len -= 2;
+
+	while (len > 0 && isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	url_start = buf;
+	while (len > 0 && !isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if ((u = calloc(1, buf - url_start + 1)) == NULL)
+		return 0;
+	memcpy(u, url_start, buf - url_start);
+
+	if (len == 0)
+		return emit_line(p, t, u, NULL);
+
+	while (len > 0) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, u, NULL);
+
+	if ((l = calloc(1, len + 1)) == NULL)
+		return 0;
+
+	memcpy(l, buf, len);
+	return emit_line(p, t, u, l);
+}
+
+static int
+parse_title(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	switch (t) {
+	case LINE_TITLE_1:
+		if (len <= 1)
+			return emit_line(p, t, NULL, NULL);
+		buf++;
+		len--;
+		break;
+	case LINE_TITLE_2:
+		if (len <= 2)
+			return emit_line(p, t, NULL, NULL);
+		buf += 2;
+		len -= 2;
+		break;
+	case LINE_TITLE_3:
+		if (len <= 3)
+			return emit_line(p, t, NULL, NULL);
+		buf += 3;
+		len -= 3;
+		break;
+	default:
+		/* unreachable */
+		abort();
+	}
+
+	while (len > 0 && isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+	memcpy(l, buf, len);
+	return emit_line(p, t, l, NULL);
+}
+
+static int
+parse_item(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	if (len == 1)
+		return emit_line(p, t, NULL, NULL);
+
+	buf++;
+	len--;
+
+	while (len > 0 && isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+	memcpy(l, buf, len);
+	return emit_line(p, t, l, NULL);
+}
+
+static int
+parse_quote(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	if (len == 1)
+		return emit_line(p, t, NULL, NULL);
+
+	buf++;
+	len--;
+
+	while (len > 0 && isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+	memcpy(l, buf, len);
+	return emit_line(p, t, l, NULL);
+}
+
+static int
+parse_pre_start(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	if (len <= 3)
+		return emit_line(p, t, NULL, NULL);
+
+	buf += 3;
+	len += 3;
+
+	while (len > 0 && isspace(buf[0])) {
+		buf++;
+		len--;
+	}
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+
+	memcpy(l, buf, len);
+	return emit_line(p, t, NULL, l);
+}
+
+static int
+parse_pre_cnt(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	char *l;
+
+	if (len == 0)
+		return emit_line(p, t, NULL, NULL);
+
+	if ((l = calloc(1, len+1)) == NULL)
+		return 0;
+	memcpy(l, buf, len);
+	return emit_line(p, t, l, NULL);
+}
+
+static int
+parse_pre_end(struct parser *p, enum line_type t, const char *buf, size_t len)
+{
+	return emit_line(p, t, NULL, NULL);
+}
+
+static inline enum line_type
+detect_line_type(const char *buf, size_t len, int in_pre)
+{
+	size_t i;
+
+	if (len == 0)
+		return LINE_TEXT;
+
+	if (in_pre) {
+		if (len >= 3 &&
+		    buf[0] == '`' && buf[1] == '`' && buf[2] == '`')
+			return LINE_PRE_END;
+		else
+			return LINE_PRE_CONTENT;
+	}
+
+	switch (*buf) {
+	case '*': return LINE_ITEM;
+	case '>': return LINE_QUOTE;
+	case '=':
+		if (len >= 1 && buf[1] == '>')
+			return LINE_LINK;
+		break;
+	case '#':
+		if (len == 1)
+			return LINE_TEXT;
+		if (buf[1] != '#')
+			return LINE_TITLE_1;
+		if (len == 2)
+			return LINE_TEXT;
+		if (buf[2] != '#')
+			return LINE_TITLE_2;
+		if (len == 3)
+			return LINE_TEXT;
+		return LINE_TITLE_3;
+	case '`':
+		if (len < 3)
+			return LINE_TEXT;
+		if (buf[0] == '`' && buf[1] == '`' && buf[2] == '`')
+			return LINE_PRE_START;
+		break;
+	}
+
+	return LINE_TEXT;
+}
+
+static inline int
+append(struct parser *p, const char *buf, size_t len)
+{
+	size_t newlen;
+	char *t;
+
+	newlen = len + p->len;
+	if ((t = calloc(1, newlen)) == NULL)
+		return 0;
+	free(p->buf);
+	p->buf = t;
+	p->len = newlen;
+	return 1;
+}
+
+static inline int
+set_buf(struct parser *p, const char *buf, size_t len)
+{
+	free(p->buf);
+	p->buf = NULL;
+
+	if (len == 0)
+		return 1;
+
+	if ((p->buf = calloc(1, len)) == NULL)
+		return 0;
+	memcpy(p->buf, buf, len);
+	return 1;
+}
+
+static int
+gemtext_parse(struct parser *p, const char *buf, size_t size)
+{
+	const char	*b, *e;
+	enum line_type	 t;
+	size_t		 len, l;
+
+	if (p->len == 0) {
+		b = buf;
+		len = size;
+	} else {
+		if (!append(p, buf, size))
+			return 0;
+		b = p->buf;
+		len = p->len;
+	}
+
+	while (len > 0) {
+		if ((e = telescope_strnchr((char*)b, '\n', len)) == NULL)
+			break;
+		l = e - b;
+		t = detect_line_type(b, l, p->flags);
+		if (t == LINE_PRE_START)
+			p->flags = 1;
+		if (t == LINE_PRE_END)
+			p->flags = 0;
+		if (!parsers[t](p, t, b, l))
+			return 0;
+
+		len -= l;
+		b += l;
+
+		if (len > 0) {
+			/* skip \n */
+			len--;
+			b++;
+		}
+	}
+
+	return set_buf(p, b, len);
+}
+
+static void
+gemtext_free(struct parser *p)
+{
+	free(p->buf);
+}
blob - /dev/null
blob + 6ff71d7a72cabe2d1707e64e327cfba3f77372a2 (mode 644)
--- /dev/null
+++ telescope.c
@@ -0,0 +1,212 @@
+#include "telescope.h"
+
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+struct event		 imsgev;
+struct tabshead		 tabshead;
+
+static struct imsgbuf	*ibuf;
+static uint32_t		 tab_counter;
+
+static void	handle_imsg_err(struct imsg*, size_t);
+static void	handle_imsg_check_cert(struct imsg*, size_t);
+static void	handle_imsg_got_code(struct imsg*, size_t);
+static void	handle_imsg_got_meta(struct imsg*, size_t);
+static void	handle_imsg_buf(struct imsg*, size_t);
+static void	handle_imsg_eof(struct imsg*, size_t);
+
+static imsg_handlerfn *handlers[] = {
+	[IMSG_ERR] = handle_imsg_err,
+	[IMSG_CHECK_CERT] = handle_imsg_check_cert,
+	[IMSG_GOT_CODE] = handle_imsg_got_code,
+	[IMSG_GOT_META] = handle_imsg_got_meta,
+	[IMSG_BUF] = handle_imsg_buf,
+	[IMSG_EOF] = handle_imsg_eof,
+};
+
+static void __attribute__((__noreturn__))
+die(void)
+{
+	abort(); 		/* TODO */
+}
+
+static struct tab *
+tab_by_id(uint32_t id)
+{
+	struct tab *t;
+
+	TAILQ_FOREACH(t, &tabshead, tabs) {
+		if (t->id == id)
+			return t;
+	}
+
+	die();
+}
+
+static void
+handle_imsg_err(struct imsg *imsg, size_t datalen)
+{
+	/* write(2, imsg->data, datalen); */
+	/* fprintf(stderr, "\nEOF\n"); */
+	/* event_loopbreak(); */
+}
+
+static void
+handle_imsg_check_cert(struct imsg *imsg, size_t datalen)
+{
+	int	tofu_res = 1;
+
+	imsg_compose(ibuf, IMSG_CERT_STATUS, imsg->hdr.peerid, 0, -1, &tofu_res, sizeof(tofu_res));
+	imsg_flush(ibuf);
+}
+
+static void
+handle_imsg_got_code(struct imsg *imsg, size_t datalen)
+{
+	int code;
+
+	if (sizeof(code) != datalen)
+		die();
+
+	memcpy(&code, imsg->data, sizeof(code));
+
+	/* fprintf(stderr, "got status code: %d\n", code); */
+}
+
+static void
+handle_imsg_got_meta(struct imsg *imsg, size_t datalen)
+{
+	/* fprintf(stderr, "got meta: "); */
+	/* fflush(stderr); */
+	/* write(2, imsg->data, datalen); */
+	/* fprintf(stderr, "\n"); */
+}
+
+static void
+handle_imsg_buf(struct imsg *imsg, size_t datalen)
+{
+        struct tab	*t;
+	struct line	*l;
+
+	t = tab_by_id(imsg->hdr.peerid);
+
+	if (!t->page.parse(&t->page, imsg->data, datalen))
+		die();
+
+	ui_on_tab_refresh(t);
+}
+
+static void
+handle_imsg_eof(struct imsg *imsg, size_t datalen)
+{
+	/* printf("===== EOF\n"); */
+	/* event_loopbreak(); */
+}
+
+static void
+dispatch_imsg(int fd, short ev, void *d)
+{
+	struct imsg	imsg;
+	size_t		datalen;
+	ssize_t		n;
+
+	if ((n = imsg_read(ibuf)) == -1) {
+		if (errno == EAGAIN || errno == EWOULDBLOCK)
+			return;
+		die();
+	}
+
+	if (n == 0) {
+		fprintf(stderr, "other side is dead\n");
+		exit(0);
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			die();
+		if (n == 0)
+			return;
+		datalen = imsg.hdr.len - IMSG_HEADER_SIZE;
+		handlers[imsg.hdr.type](&imsg, datalen);
+		imsg_free(&imsg);
+	}
+}
+
+void
+new_tab(void)
+{
+	struct tab	*tab;
+	const char	*url = "about:new";
+	/* const char	*url = "gemini://localhost/cgi/slow-out"; */
+
+	if ((tab = calloc(1, sizeof(*tab))) == NULL)
+		die();
+
+	TAILQ_INSERT_HEAD(&tabshead, tab, tabs);
+
+	tab->id = tab_counter++;
+	TAILQ_INIT(&tab->page.head);
+	gemtext_initparser(&tab->page);
+
+	imsg_compose(ibuf, IMSG_GET, tab->id, 0, -1, url, strlen(url)+1);
+	imsg_flush(ibuf);
+
+	ui_on_new_tab(tab);
+}
+
+int
+main(void)
+{
+	struct imsgbuf	main_ibuf, network_ibuf;
+	int		imsg_fds[2];
+
+	signal(SIGCHLD, SIG_IGN);
+	signal(SIGINT, SIG_IGN);
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, imsg_fds) == -1)
+		err(1, "socketpair");
+
+	switch (fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		/* child */
+		setproctitle("client");
+		close(imsg_fds[0]);
+		imsg_init(&network_ibuf, imsg_fds[1]);
+		exit(client_main(&network_ibuf));
+	}
+
+	close(imsg_fds[1]);
+	imsg_init(&main_ibuf, imsg_fds[0]);
+	ibuf = &main_ibuf;
+
+	TAILQ_INIT(&tabshead);
+
+	event_init();
+
+	event_set(&imsgev, ibuf->fd, EV_READ | EV_PERSIST, dispatch_imsg, ibuf);
+	event_add(&imsgev, NULL);
+
+	ui_init();
+
+	new_tab();
+
+	event_dispatch();
+
+	imsg_compose(ibuf, IMSG_QUIT, 0, 0, -1, NULL, 0);
+	imsg_flush(ibuf);
+
+	ui_end();
+
+	return 0;
+}
blob - /dev/null
blob + 785fae6dcf27d3a61d967a2b59313424376458e8 (mode 644)
--- /dev/null
+++ telescope.h
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef TELESCOPE_H
+#define TELESCOPE_H
+
+#include <config.h>
+#include <url.h>
+
+/* TODO: move in config.h */
+#include <sys/queue.h>
+#include <sys/types.h>
+#include <sys/uio.h>
+#include <stdint.h>
+#include <imsg.h>
+
+#define GEMINI_URL_LEN 1024
+
+enum imsg_type {
+	IMSG_GET,		/* data is URL, peerid the tab id */
+	IMSG_ERR,
+	IMSG_CHECK_CERT,
+	IMSG_CERT_STATUS,
+	IMSG_GOT_CODE,
+	IMSG_GOT_META,
+	IMSG_BUF,
+	IMSG_EOF,
+	IMSG_STOP,
+	IMSG_QUIT,
+};
+
+enum line_type {
+	LINE_TEXT,
+	LINE_LINK,
+	LINE_TITLE_1,
+	LINE_TITLE_2,
+	LINE_TITLE_3,
+	LINE_ITEM,
+	LINE_QUOTE,
+	LINE_PRE_START,
+	LINE_PRE_CONTENT,
+	LINE_PRE_END,
+};
+
+struct line {
+	enum line_type		 type;
+	char			*line;
+	char			*alt;
+	TAILQ_ENTRY(line)	 lines;
+};
+
+struct parser;
+struct page;
+
+/* typedef void	(*initparserfn)(struct parser*); */
+
+typedef int	(*parsechunkfn)(struct parser*, const char*, size_t);
+typedef void	(*parserfreefn)(struct parser*);
+
+typedef void (imsg_handlerfn)(struct imsg*, size_t);
+
+struct parser {
+	char		*buf;
+	size_t		 len;
+	size_t		 cap;
+	int		 flags;
+	parsechunkfn	 parse;
+	parserfreefn	 free;
+
+	TAILQ_HEAD(, line)	 head;
+};
+
+extern TAILQ_HEAD(tabshead, tab) tabshead;
+struct tab {
+	struct parser		 page;
+	TAILQ_ENTRY(tab)	 tabs;
+	uint32_t		 id;
+	uint32_t		 flags;
+};
+
+extern struct event		 imsgev;
+
+/* about.c */
+extern const char	*about_new;
+
+/* gemini.c */
+int		 client_main(struct imsgbuf *b);
+
+/* gemtext.c */
+void		 gemtext_initparser(struct parser*);
+
+/* ui.c */
+int		 ui_init(void);
+void		 ui_on_new_tab(struct tab*);
+void		 ui_on_tab_refresh(struct tab*);
+void		 ui_end(void);
+
+/* util.c */
+int		 mark_nonblock(int);
+char		*telescope_strnchr(char*, char, size_t);
+
+#endif /* TELESCOPE_H */
blob - /dev/null
blob + 99baa1da940b355e5a271985a179ba5be3d50e8e (mode 644)
--- /dev/null
+++ ui.c
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <telescope.h>
+
+#include <curses.h>
+#include <event.h>
+#include <locale.h>
+#include <signal.h>
+
+#define TAB_CURRENT	0x1
+
+static struct event	stdioev, winchev;
+
+static struct tab	*current_tab(void);
+static void		 dispatch_stdio(int, short, void*);
+static void		 handle_resize(int, short, void*);
+static void		 redraw_tab(struct tab*);
+
+static struct tab *
+current_tab(void)
+{
+	struct tab *t;
+
+	TAILQ_FOREACH(t, &tabshead, tabs) {
+		if (t->flags & TAB_CURRENT)
+			return t;
+	}
+
+	/* unreachable */
+	abort();
+}
+
+static void
+dispatch_stdio(int fd, short ev, void *d)
+{
+	int c;
+
+	c = getch();
+
+	if (c == ERR)
+		return;
+
+	if (c == 'q') {
+		event_loopbreak();
+		return;
+	}
+
+	printw("You typed %c\n", c);
+	refresh();
+}
+
+static void
+handle_resize(int sig, short ev, void *d)
+{
+	endwin();
+	refresh();
+	clear();
+
+	redraw_tab(current_tab());
+}
+
+static void
+redraw_tab(struct tab *tab)
+{
+	struct line	*l;
+
+	erase();
+
+	TAILQ_FOREACH(l, &tab->page.head, lines) {
+		switch (l->type) {
+		case LINE_TEXT:
+			printw("%s\n", l->line);
+			break;
+		case LINE_LINK:
+			printw("=> %s\n", l->line);
+			break;
+		case LINE_TITLE_1:
+			printw("# %s\n", l->line);
+			break;
+		case LINE_TITLE_2:
+			printw("## %s\n", l->line);
+			break;
+		case LINE_TITLE_3:
+			printw("### %s\n", l->line);
+			break;
+		case LINE_ITEM:
+			printw("* %s\n", l->line);
+			break;
+		case LINE_QUOTE:
+			printw("> %s\n", l->line);
+			break;
+		case LINE_PRE_START:
+		case LINE_PRE_END:
+			printw("```\n");
+			break;
+		case LINE_PRE_CONTENT:
+			printw("`%s\n", l->line);
+			break;
+		}
+	}
+
+	refresh();
+}
+
+int
+ui_init(void)
+{
+	setlocale(LC_ALL, "");
+
+	initscr();
+	cbreak();
+	noecho();
+
+	nonl();
+	intrflush(stdscr, FALSE);
+	keypad(stdscr, TRUE);
+
+	/* non-blocking input */
+	timeout(0);
+
+	mvprintw(0, 0, "");
+
+	event_set(&stdioev, 0, EV_READ | EV_PERSIST, dispatch_stdio, NULL);
+	event_add(&stdioev, NULL);
+
+	signal_set(&winchev, SIGWINCH, handle_resize, NULL);
+	signal_add(&winchev, NULL);
+
+	return 1;
+}
+
+void
+ui_on_new_tab(struct tab *tab)
+{
+	struct tab	*t;
+
+	TAILQ_FOREACH(t, &tabshead, tabs) {
+		t->flags &= ~TAB_CURRENT;
+	}
+
+	tab->flags = TAB_CURRENT;
+
+	/* TODO: redraw the tab list */
+}
+
+void
+ui_on_tab_refresh(struct tab *tab)
+{
+	if (!(tab->flags & TAB_CURRENT))
+		return;
+
+	redraw_tab(tab);
+}
+
+void
+ui_end(void)
+{
+	endwin();
+}
+
blob - /dev/null
blob + c1931a699d4fd7f28ab71456257f3c97eceb250d (mode 644)
--- /dev/null
+++ url.c
@@ -0,0 +1,436 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * This is mostly copied from gmid' iri.c, with minor changes.  Bugs
+ * fixed here should be ported to gmid and vice-versa.
+ */
+
+#include <config.h>
+#include <url.h>
+
+#include <ctype.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+struct shallow_url {
+	char			*scheme;
+	char			*host;
+	char			*port;
+	uint16_t		 port_no;
+	char			*path;
+	char			*query;
+	char			*fragment;
+};
+
+struct parser {
+	char			 buf[GEMINI_URL_LEN+1];
+	char			*iri;
+	struct shallow_url	*parsed;
+	const char		*err;
+};
+
+static inline int
+unreserved(int p)
+{
+	return isalnum(p)
+		|| p == '-'
+		|| p == '.'
+		|| p == '_'
+		|| p == '~';
+}
+
+static inline int
+sub_delimiters(int p)
+{
+	return p == '!'
+		|| p == '$'
+		|| p == '&'
+		|| p == '\''
+		|| p == '('
+		|| p == ')'
+		|| p == '*'
+		|| p == '+'
+		|| p == ','
+		|| p == ';'
+		|| p == '=';
+}
+
+static int
+valid_pct_enc_string(char *s)
+{
+	if (*s != '%')
+		return 1;
+
+	if (!isxdigit(s[1]) || !isxdigit(s[2]))
+		return 0;
+
+	if (s[1] == '0' && s[2] == '0')
+		return 0;
+
+	return 1;
+}
+
+static int
+valid_pct_encoded(struct parser *p)
+{
+	if (p->iri[0] != '%')
+		return 0;
+
+	if (!valid_pct_enc_string(p->iri)) {
+		p->err = "illegal percent-encoding";
+		return 0;
+	}
+
+	p->iri += 2;
+	return 1;
+}
+
+static void
+pct_decode(char *s)
+{
+	sscanf(s+1, "%2hhx", s);
+	memmove(s+1, s+3, strlen(s+3)+1);
+}
+
+static int
+parse_pct_encoded(struct parser *p)
+{
+	if (p->iri[0] != '%')
+		return 0;
+
+	if (!valid_pct_enc_string(p->iri)) {
+		p->err = "illegal percent-encoding";
+		return 0;
+	}
+
+	pct_decode(p->iri);
+	if (*p->iri == '\0') {
+		p->err = "illegal percent-encoding";
+		return 0;
+	}
+
+	return 1;
+}
+
+/* ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) "://" */
+static int
+parse_scheme(struct parser *p)
+{
+	p->parsed->scheme = p->iri;
+
+	if (!isalpha(*p->iri)) {
+		p->err = "illegal character in scheme";
+		return 0;
+	}
+
+	do {
+		/* normalize the scheme (i.e. lowercase it)
+		 *
+		 * XXX: since we cannot have good things, tolower
+		 * behaviour depends on the LC_CTYPE locale.  The good
+		 * news is that we're sure p->iri points to something
+		 * that's in the ASCII range, so tolower can't
+		 * mis-behave on some systems due to the locale. */
+		*p->iri = tolower(*p->iri);
+		p->iri++;
+	} while (isalnum(*p->iri)
+	    || *p->iri == '+'
+	    || *p->iri == '-'
+	    || *p->iri == '.');
+
+	if (*p->iri != ':') {
+		p->err = "illegal character in scheme";
+		return 0;
+	}
+
+	*p->iri = '\0';
+	if (p->iri[1] != '/' || p->iri[2] != '/') {
+		p->err = "invalid marker after scheme";
+		return 0;
+	}
+
+	p->iri += 3;
+	return 1;
+}
+
+/* *DIGIT */
+static int
+parse_port(struct parser *p)
+{
+	uint32_t i = 0;
+
+	p->parsed->port = p->iri;
+
+	for (; isdigit(*p->iri); p->iri++) {
+		i = i * 10 + *p->iri - '0';
+		if (i > UINT16_MAX) {
+			p->err = "port number too large";
+			return 0;
+		}
+	}
+
+	if (*p->iri != '/' && *p->iri != '\0') {
+		p->err = "illegal character in port number";
+		return 0;
+	}
+
+	p->parsed->port_no = i;
+
+	if (*p->iri != '\0') {
+		*p->iri = '\0';
+		p->iri++;
+	}
+
+	return 1;
+}
+
+/* TODO: add support for ip-literal and ipv4addr ? */
+/* *( unreserved / sub-delims / pct-encoded ) */
+static int
+parse_authority(struct parser *p)
+{
+	p->parsed->host = p->iri;
+
+	while (unreserved(*p->iri)
+	    || sub_delimiters(*p->iri)
+	    || parse_pct_encoded(p)) {
+		/* normalize the host name. */
+		if (*p->iri < 0x7F)
+			*p->iri = tolower(*p->iri);
+		p->iri++;
+	}
+
+	if (p->err != NULL)
+		return 0;
+
+	if (*p->iri == ':') {
+		*p->iri = '\0';
+		p->iri++;
+		return parse_port(p);
+	} else
+		p->parsed->port_no = 1965;
+
+	if (*p->iri == '/') {
+		*p->iri = '\0';
+		p->iri++;
+		return 1;
+	}
+
+	if (*p->iri == '\0')
+		return 1;
+
+	p->err = "illegal character in authority section";
+	return 0;
+}
+
+/* Routine for path_clean.  Elide the pointed .. with the preceding
+ * element.  Return 0 if it's not possible.  incr is the length of
+ * the increment, 3 for ../ and 2 for .. */
+static int
+path_elide_dotdot(char *path, char *i, int incr)
+{
+	char *j;
+
+	if (i == path)
+		return 0;
+	for (j = i-2; j != path && *j != '/'; j--)
+                /* noop */ ;
+	if (*j == '/')
+		j++;
+	i += incr;
+	memmove(j, i, strlen(i)+1);
+	return 1;
+}
+
+/*
+ * Use an algorithm similar to the one implemented in go' path.Clean:
+ *
+ * 1. Replace multiple slashes with a single slash
+ * 2. Eliminate each . path name element
+ * 3. Eliminate each inner .. along with the non-.. element that precedes it
+ * 4. Eliminate trailing .. if possible or error (go would only discard)
+ *
+ * Unlike path.Clean, this function return the empty string if the
+ * original path is equivalent to "/".
+ */
+static int
+path_clean(char *path)
+{
+	char *i;
+
+	/* 1. replace multiple slashes with a single one */
+	for (i = path; *i; ++i) {
+		if (*i == '/' && *(i+1) == '/') {
+			memmove(i, i+1, strlen(i)); /* move also the \0 */
+			i--;
+		}
+	}
+
+	/* 2. eliminate each . path name element */
+	for (i = path; *i; ++i) {
+		if ((i == path || *i == '/') &&
+		    *i != '.' && i[1] == '.' && i[2] == '/') {
+			/* move also the \0 */
+			memmove(i, i+2, strlen(i)-1);
+			i--;
+		}
+	}
+	if (!strcmp(path, ".") || !strcmp(path, "/.")) {
+		*path = '\0';
+		return 1;
+	}
+
+	/* 3. eliminate each inner .. along with the preceding non-.. */
+	for (i = strstr(path, "../"); i != NULL; i = strstr(path, ".."))
+		if (!path_elide_dotdot(path, i, 3))
+			return 0;
+
+	/* 4. eliminate trailing ..*/
+	if ((i = strstr(path, "..")) != NULL)
+		if (!path_elide_dotdot(path, i, 2))
+			return 0;
+
+	return 1;
+}
+
+static int
+parse_query(struct parser *p)
+{
+	p->parsed->query = p->iri;
+	if (*p->iri == '\0')
+		return 1;
+
+	while (unreserved(*p->iri)
+	    || sub_delimiters(*p->iri)
+	    || *p->iri == '/'
+	    || *p->iri == '?'
+	    || *p->iri == ':'
+	    || *p->iri == '@'
+	    || valid_pct_encoded(p))
+		p->iri++;
+
+	if (p->err != NULL)
+		return 0;
+
+	if (*p->iri != '\0' && *p->iri != '#') {
+		p->err = "illegal character in query";
+		return 0;
+	}
+
+	if (*p->iri != '\0') {
+		*p->iri = '\0';
+		p->iri++;
+	}
+
+	return 1;
+}
+
+/* don't even bother */
+static int
+parse_fragment(struct parser *p)
+{
+	p->parsed->fragment = p->iri;
+	return 1;
+}
+
+/* XXX: is it too broad? */
+/* *(pchar / "/") */
+static int
+parse_path(struct parser *p)
+{
+	char c;
+
+	/* trim initial slashes */
+	while (*p->iri == '/')
+		p->iri++;
+
+	p->parsed->path = p->iri;
+	if (*p->iri == '\0') {
+		p->parsed->query = p->parsed->fragment = p->iri;
+		return 1;
+	}
+
+	while (unreserved(*p->iri)
+	    || sub_delimiters(*p->iri)
+	    || *p->iri == '/'
+	    || parse_pct_encoded(p))
+		p->iri++;
+
+	if (p->err != NULL)
+		return 0;
+
+	if (*p->iri != '\0' && *p->iri != '?' && *p->iri != '#') {
+		p->err = "illegal character in path";
+		return 0;
+	}
+
+	if (*p->iri != '\0') {
+		c = *p->iri;
+		*p->iri = '\0';
+		p->iri++;
+
+		if (c == '#') {
+			if (!parse_fragment(p))
+				return 0;
+		} else
+			if (!parse_query(p) || !parse_fragment(p))
+				return 0;
+	}
+
+	if (!path_clean(p->parsed->path)) {
+		p->err = "illegal path";
+		return 0;
+	}
+
+	return 1;
+}
+
+int
+url_parse(const char *data, struct url *url, const char **err)
+{
+	struct shallow_url	u;
+	struct parser		p;
+
+	memset(url, 0, sizeof(*url));
+	memset(&p, 0, sizeof(p));
+	memset(&u, 0, sizeof(u));
+
+	strlcpy(p.buf, data, sizeof(p.buf));
+	p.iri = p.buf;
+	p.parsed = &u;
+
+	if (!parse_scheme(&p) || !parse_authority(&p) || !parse_path(&p)) {
+		*err = p.err;
+		return 0;
+	}
+
+	if (u.scheme != NULL)
+		strlcpy(url->scheme, u.scheme, sizeof(url->scheme));
+	if (u.host != NULL)
+		strlcpy(url->host, u.host, sizeof(url->host));
+	if (u.port != NULL)
+		strlcpy(url->port, u.port, sizeof(url->port));
+	if (u.path != NULL)
+		strlcpy(url->path, u.path, sizeof(url->path));
+	if (u.query != NULL)
+		strlcpy(url->query, u.query, sizeof(url->query));
+	if (u.fragment != NULL)
+		strlcpy(url->fragment, u.fragment, sizeof(url->fragment));
+
+	return 1;
+}
blob - /dev/null
blob + b43ea710c8ecda1d6981658ecdab720281d9da80 (mode 644)
--- /dev/null
+++ url.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <netdb.h>
+
+#define GEMINI_URL_LEN 1024
+
+/* +1 for NUL */
+struct url {
+	char	scheme[32];
+	char	host[NI_MAXHOST+1];
+	char	port[NI_MAXSERV+1];
+	char	path[GEMINI_URL_LEN+1];
+	char	query[GEMINI_URL_LEN+1];
+	char	fragment[GEMINI_URL_LEN+1];
+};
+
+int		 url_parse(const char*, struct url*, const char**);
blob - /dev/null
blob + 341da330169c7050c05b46fbe6337da7c133f590 (mode 644)
--- /dev/null
+++ utf8.c
@@ -0,0 +1,96 @@
+/* Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ *
+ * 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.
+ */
+
+#include "gmid.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#define UTF8_ACCEPT 0
+#define UTF8_REJECT 1
+
+static const uint8_t utf8d[] = {
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 00..1f
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 20..3f
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 40..5f
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // 60..7f
+	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9, // 80..9f
+	7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, // a0..bf
+	8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, // c0..df
+	0xa,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x3,0x4,0x3,0x3, // e0..ef
+	0xb,0x6,0x6,0x6,0x5,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8,0x8, // f0..ff
+	0x0,0x1,0x2,0x3,0x5,0x8,0x7,0x1,0x1,0x1,0x4,0x6,0x1,0x1,0x1,0x1, // s0..s0
+	1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,0,1,0,1,1,1,1,1,1, // s1..s2
+	1,2,1,1,1,1,1,2,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1, // s3..s4
+	1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,1,1,1,3,1,3,1,1,1,1,1,1, // s5..s6
+	1,3,1,1,1,1,1,3,1,3,1,1,1,1,1,1,1,3,1,1,1,1,1,1,1,1,1,1,1,1,1,1, // s7..s8
+};
+
+static inline uint32_t
+utf8_decode(uint32_t* state, uint32_t* codep, uint8_t byte) {
+	uint32_t type = utf8d[byte];
+
+	*codep = (*state != UTF8_ACCEPT) ?
+		(byte & 0x3fu) | (*codep << 6) :
+		(0xff >> type) & (byte);
+
+	*state = utf8d[256 + *state*16 + type];
+	return *state;
+}
+
+/* for the iri parser.  Modelled after printCodePoints */
+int
+valid_multibyte_utf8(struct parser *p)
+{
+	uint32_t cp = 0, state = 0;
+
+        for (; *p->iri; p->iri++)
+		if (!utf8_decode(&state, &cp, *p->iri))
+			break;
+
+	/* reject the ASCII range */
+	if (state || cp <= 0x7F) {
+		/* XXX: do some error recovery? */
+		if (state)
+			p->err = "invalid UTF-8 character";
+		return 0;
+	}
+	return 1;
+}
+
+char *
+utf8_nth(char *s, size_t n)
+{
+	size_t i;
+	uint32_t cp = 0, state = 0;
+
+	for (i = 0; *s && i < n; ++s)
+		if (!utf8_decode(&state, &cp, *s))
+			++i;
+
+	if (state != UTF8_ACCEPT)
+		return NULL;
+	if (i == n)
+		return s;
+	return NULL;
+}
blob - /dev/null
blob + 7a56d2d26e7ab2c29c2f604d20bef208c3415dd1 (mode 644)
--- /dev/null
+++ util.c
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "telescope.h"
+
+#include <fcntl.h>
+
+int
+mark_nonblock(int fd)
+{
+	int flags;
+
+	if ((flags = fcntl(fd, F_GETFL)) == -1)
+                return 0;
+	if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
+                return 0;
+	return 1;
+}
+
+char *
+telescope_strnchr(char *b, char d, size_t len)
+{
+	size_t i;
+
+	for (i = 0; i < len; ++i) {
+		if (b[i] == d)
+			return &b[i];
+	}
+
+	return NULL;
+}