diff --git a/Makefile b/Makefile

index 69a241a8825a6a7aa979eb2ae95a26faaf3a0532..3d4df602cd7f7ab4f5a45b47dee0d47729f0739c 100644

--- a/Makefile

+++ b/Makefile

@@ -8,6 +8,10 @@ gmni: $(gmni_objects)

@printf 'CCLD\t$@\n'

@$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmni_objects)

+gmnlm: $(gmnlm_objects)

+ @printf 'CCLD\t$@\n'

+ @$(CC) $(LDFLAGS) $(LIBS) -o $@ $(gmnlm_objects)

+

doc/gmni.1: doc/gmni.scd

.SUFFIXES: .c .o .scd .1

diff --git a/config.sh b/config.sh

index b93815ada4a25ec508e7a86cc79b9e9be3eba428..52931ab241c3177285b56258bf9b67ac4b63a7ea 100644

--- a/config.sh

+++ b/config.sh

@@ -134,6 +134,7 @@

all: ${all}

EOF

gmni >>"$outdir"/config.mk

+ gmnlm >>"$outdir"/config.mk

echo done

touch $outdir/cppcache

diff --git a/configure b/configure

index aca6e8a1eaa9a6271f03eb8863640d0d93cdf435..151bdae8c12d9ad07d3a5240d7c554f4c62664c8 100755

--- a/configure

+++ b/configure

@@ -7,10 +7,18 @@ genrules gmni \

src/client.c \

src/escape.c \

src/gmni.c \

+ src/url.c

+}

+

+gmnlm() {

+ genrules gmnlm \

+ src/client.c \

+ src/escape.c \

+ src/gmnlm.c \

src/parser.c \

src/url.c

}

-all="gmni"

+all="gmni gmnlm"

run_configure

diff --git a/src/gmni.c b/src/gmni.c

index 75c6c5afb6e7f1f286d314d93f2b44ef8414afb3..dc0c5c7f61679369fe4fadafd0508147868cc3c3 100644

--- a/src/gmni.c

+++ b/src/gmni.c

@@ -15,7 +15,7 @@ #include <unistd.h>

#include "gmni.h"

static void

-usage(char *argv_0)

+usage(const char *argv_0)

{

fprintf(stderr,

"usage: %s [-46lLiIN] [-E cert] [-d input] [-D path] gemini://...\n",

diff --git a/src/gmnlm.c b/src/gmnlm.c

new file mode 100644

index 0000000000000000000000000000000000000000..bc3f647d42372678b4180f539df8637d0ba69a12

--- /dev/null

+++ b/src/gmnlm.c

@@ -0,0 +1,215 @@

+#include <assert.h>

+#include <ctype.h>

+#include <getopt.h>

+#include <openssl/bio.h>

+#include <openssl/err.h>

+#include <stdbool.h>

+#include <stdio.h>

+#include <string.h>

+#include <sys/ioctl.h>

+#include <termios.h>

+#include <unistd.h>

+#include "gmni.h"

+#include "url.h"

+

+struct link {

+ char *url;

+ struct link *next;

+};

+

+static void

+usage(const char *argv_0)

+{

+ fprintf(stderr, "usage: %s [gemini://...]\n", argv_0);

+}

+

+static bool

+set_url(struct Curl_URL *url, char *new_url)

+{

+ if (curl_url_set(url, CURLUPART_URL, new_url, 0) != CURLUE_OK) {

+ fprintf(stderr, "Error: invalid URL\n");

+ return false;

+ }

+ return true;

+}

+

+static char *

+trim_ws(char *in)

+{

+ for (int i = strlen(in) - 1; in[i] && isspace(in[i]); --i) {

+ in[i] = 0;

+ }

+ for (; *in && isspace(*in); ++in);

+ return in;

+}

+

+static void

+display_gemini(FILE *tty, struct gemini_response *resp,

+ struct link **next, bool pagination)

+{

+ int nlinks = 0;

+ struct gemini_parser p;

+ gemini_parser_init(&p, resp->bio);

+

+ struct winsize ws;

+ ioctl(fileno(tty), TIOCGWINSZ, &ws);

+

+ int row = 0, col = 0;

+ struct gemini_token tok;

+ while (gemini_parser_next(&p, &tok) == 0) {

+ switch (tok.token) {

+ case GEMINI_TEXT:

+ // TODO: word wrap

+ col += fprintf(tty, " %s\n", trim_ws(tok.text));

+ break;

+ case GEMINI_LINK:

+ (void)next; // TODO: Record links

+ col += fprintf(tty, "[%d] %s\n", nlinks++, trim_ws(

+ tok.link.text ? tok.link.text : tok.link.url));

+ break;

+ case GEMINI_PREFORMATTED:

+ continue; // TODO

+ case GEMINI_HEADING:

+ for (int n = tok.heading.level; n; --n) {

+ col += fprintf(tty, "#");

+ }

+ col += fprintf(tty, " %s\n", trim_ws(tok.heading.title));

+ break;

+ case GEMINI_LIST_ITEM:

+ // TODO: Option to disable Unicode

+ col += fprintf(tty, " • %s\n", trim_ws(tok.list_item));

+ break;

+ case GEMINI_QUOTE:

+ // TODO: Option to disable Unicode

+ col += fprintf(tty, " | %s\n", trim_ws(tok.quote_text));

+ break;

+ }

+

+ while (col >= ws.ws_col) {

+ col -= ws.ws_col;

+ ++row;

+ }

+ ++row;

+ col = 0;

+

+ if (pagination && row >= ws.ws_row - 1) {

+ fprintf(tty, "[Enter for more, or q to stop] ");

+

+ size_t n = 0;

+ char *l = NULL;

+ if (getline(&l, &n, tty) == -1) {

+ return;

+ }

+ if (strcmp(l, "q\n") == 0) {

+ return;

+ }

+

+ free(l);

+ row = col = 0;

+ }

+ }

+

+ gemini_parser_finish(&p);

+}

+

+int

+main(int argc, char *argv[])

+{

+ bool pagination = true;

+

+ bool have_url = false;

+ struct Curl_URL *url = curl_url();

+

+ FILE *tty = fopen("/dev/tty", "w+");

+

+ int c;

+ while ((c = getopt(argc, argv, "hP")) != -1) {

+ switch (c) {

+ case 'P':

+ pagination = false;

+ break;

+ case 'h':

+ usage(argv[0]);

+ return 0;

+ default:

+ fprintf(stderr, "fatal: unknown flag %c\n", c);

+ return 1;

+ }

+ }

+

+ if (optind == argc - 1) {

+ set_url(url, argv[optind]);

+ have_url = true;

+ } else if (optind < argc - 1) {

+ usage(argv[0]);

+ return 1;

+ }

+

+ SSL_load_error_strings();

+ ERR_load_crypto_strings();

+ struct gemini_options opts = {

+ .ssl_ctx = SSL_CTX_new(TLS_method()),

+ };

+

+ bool run = true;

+ struct gemini_response resp;

+ while (run) {

+ assert(have_url); // TODO

+

+ struct link *links;

+ static char prompt[4096];

+

+ char *plain_url;

+ CURLUcode uc = curl_url_get(url, CURLUPART_URL, &plain_url, 0);

+ assert(uc == CURLUE_OK); // Invariant

+

+ snprintf(prompt, sizeof(prompt), "\n\t%s\n"

+ "\tWhere to? [n]: follow Nth link; [o <url>]: open URL; [q]: quit\n"

+ "=> ", plain_url);

+

+ enum gemini_result res = gemini_request(plain_url, &opts, &resp);

+ if (res != GEMINI_OK) {

+ fprintf(stderr, "Error: %s\n", gemini_strerr(res, &resp));

+ assert(0); // TODO: Prompt

+ }

+

+ switch (gemini_response_class(resp.status)) {

+ case GEMINI_STATUS_CLASS_INPUT:

+ assert(0); // TODO

+ case GEMINI_STATUS_CLASS_REDIRECT:

+ assert(0); // TODO

+ case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:

+ assert(0); // TODO

+ case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:

+ case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:

+ fprintf(stderr, "Server returned %s %d %s\n",

+ resp.status / 10 == 4 ?

+ "TEMPORARY FAILURE" : "PERMANENT FALIURE",

+ resp.status, resp.meta);

+ break;

+ case GEMINI_STATUS_CLASS_SUCCESS:

+ display_gemini(tty, &resp, &links, pagination);

+ break;

+ }

+

+ gemini_response_finish(&resp);

+

+ fprintf(tty, "%s", prompt);

+ size_t l = 0;

+ char *in = NULL;

+ ssize_t n = getline(&in, &l, tty);

+ if (n == -1 && feof(tty)) {

+ break;

+ }

+

+ if (strcmp(in, "q\n") == 0) {

+ run = false;

+ }

+

+ free(in);

+ }

+

+ SSL_CTX_free(opts.ssl_ctx);

+ curl_url_cleanup(url);

+ return 0;

+}