diff --git a/configure b/configure

index 151bdae8c12d9ad07d3a5240d7c554f4c62664c8..7412cf545fa96e6be169c0055778b83a06b557f5 100755

--- a/configure

+++ b/configure

@@ -16,7 +16,8 @@ src/client.c \

src/escape.c \

src/gmnlm.c \

src/parser.c \

- src/url.c

+ src/url.c \

+ src/util.c

}

all="gmni gmnlm"

diff --git a/include/util.h b/include/util.h

new file mode 100644

index 0000000000000000000000000000000000000000..cf731bfdc75a750df6f18873f48dd859786587c7

--- /dev/null

+++ b/include/util.h

@@ -0,0 +1,12 @@

+#ifndef GEMINI_UTIL_H

+#define GEMINI_UTIL_H

+

+struct pathspec {

+ const char *var;

+ const char *path;

+};

+

+char *getpath(const struct pathspec *paths, size_t npaths);

+int mkdirs(char *path, mode_t mode);

+

+#endif

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

index 4b844200eeb4318d694566d7a84eb3e64c7c7668..c5d5da36ffad8772315928e8a69b785d994511e4 100644

--- a/src/gmnlm.c

+++ b/src/gmnlm.c

@@ -12,6 +12,7 @@ #include <termios.h>

#include <unistd.h>

#include "gmni.h"

#include "url.h"

+#include "util.h"

struct link {

char *url;

@@ -29,6 +30,7 @@ struct gemini_options opts;

FILE *tty;

char *plain_url;

+ char *page_title;

struct Curl_URL *url;

struct link *links;

struct history *history;

@@ -52,6 +54,8 @@ "q\tQuit\n"

"N\tFollow Nth link (where N is a number)\n"

"b\tBack (in the page history)\n"

"f\tForward (in the page history)\n"

+ "m\tSave bookmark\n"

+ "M\tBrowse bookmarks\n"

"\n"

"Other commands include:\n\n"

"<Enter>\tread more lines\n"

@@ -98,6 +102,63 @@ }

return true;

}

+static char *

+get_data_pathfmt()

+{

+ const struct pathspec paths[] = {

+ {.var = "GMNIDATA", .path = "/%s"},

+ {.var = "XDG_DATA_HOME", .path = "/gmni/%s"},

+ {.var = "HOME", .path = "/.local/share/gmni/%s"}

+ };

+ return getpath(paths, sizeof(paths) / sizeof(paths[0]));

+}

+

+static char *

+trim_ws(char *in)

+{

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

+ return in;

+}

+

+static void

+save_bookmark(struct browser *browser)

+{

+ const char *path_fmt = get_data_pathfmt();

+ static char path[PATH_MAX+1];

+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");

+ mkdirs(path, 0755);

+

+ FILE *f = fopen(path, "a");

+ if (!f) {

+ fprintf(stderr, "Error opening %s for writing: %s\n",

+ path, strerror(errno));

+ return;

+ }

+

+ char *title = browser->page_title;

+ if (title) {

+ title = trim_ws(browser->page_title);

+ }

+

+ fprintf(f, "=> %s%s%s\n", browser->plain_url,

+ title ? " " : "", title ? title : "");

+ fclose(f);

+

+ fprintf(browser->tty, "Bookmark saved: %s\n",

+ title ? title : browser->plain_url);

+}

+

+static void

+open_bookmarks(struct browser *browser)

+{

+ const char *path_fmt = get_data_pathfmt();

+ static char path[PATH_MAX+1];

+ snprintf(path, sizeof(path), path_fmt, "bookmarks.gmi");

+ static char url[PATH_MAX+1+7];

+ snprintf(url, sizeof(url), "file://%s", path);

+ set_url(browser, url, &browser->history);

+}

+

static enum prompt_result

do_prompts(const char *prompt, struct browser *browser)

{

@@ -134,6 +195,16 @@ browser->history = browser->history->prev;

set_url(browser, browser->history->url, NULL);

result = PROMPT_ANSWERED;

goto exit;

+ case 'm':

+ if (in[1]) break;

+ save_bookmark(browser);

+ result = PROMPT_AGAIN;

+ goto exit;

+ case 'M':

+ if (in[1]) break;

+ open_bookmarks(browser);

+ result = PROMPT_ANSWERED;

+ goto exit;

case 'f':

if (in[1]) break;

if (!browser->history->next) {

@@ -205,13 +276,6 @@ free(in);

return result;

}

-static char *

-trim_ws(char *in)

-{

- while (*in && isspace(*in)) ++in;

- return in;

-}

-

static int

wrap(FILE *f, char *s, struct winsize *ws, int *row, int *col)

{

@@ -258,6 +322,8 @@ {

int nlinks = 0;

struct gemini_parser p;

gemini_parser_init(&p, resp->bio);

+ free(browser->page_title);

+ browser->page_title = NULL;

struct winsize ws;

ioctl(fileno(browser->tty), TIOCGWINSZ, &ws);

@@ -302,6 +368,9 @@ text = tok.text;

}

break;

case GEMINI_HEADING:

+ if (!browser->page_title) {

+ browser->page_title = strdup(tok.heading.title);

+ }

if (text == NULL) {

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

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

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

new file mode 100644

index 0000000000000000000000000000000000000000..26e7283a26cc8e593bb3e3fd53d4b7a9bc646e3f

--- /dev/null

+++ b/src/util.c

@@ -0,0 +1,62 @@

+#include <assert.h>

+#include <errno.h>

+#include <libgen.h>

+#include <limits.h>

+#include <stdlib.h>

+#include <string.h>

+#include <sys/stat.h>

+#include "util.h"

+

+static void

+posix_dirname(char *path, char *dname)

+{

+ char p[PATH_MAX+1];

+ char *t;

+

+ assert(strlen(path) <= PATH_MAX);

+

+ strcpy(p, path);

+ t = dirname(path);

+ memmove(dname, t, strlen(t) + 1);

+

+ /* restore the path if dirname worked in-place */

+ if (t == path && path != dname) {

+ strcpy(path, p);

+ }

+}

+

+/** Make directory and all of its parents */

+int

+mkdirs(char *path, mode_t mode)

+{

+ char dname[PATH_MAX + 1];

+ posix_dirname(path, dname);

+ if (strcmp(dname, "/") == 0) {

+ return 0;

+ }

+ if (mkdirs(dname, mode) != 0) {

+ return -1;

+ }

+ if (mkdir(path, mode) != 0 && errno != EEXIST) {

+ return -1;

+ }

+ errno = 0;

+ return 0;

+}

+

+char *

+getpath(const struct pathspec *paths, size_t npaths) {

+ for (size_t i = 0; i < npaths; i++) {

+ const char *var = "";

+ if (paths[i].var) {

+ var = getenv(paths[i].var);

+ }

+ if (var) {

+ char *out = calloc(1,

+ strlen(var) + strlen(paths[i].path) + 1);

+ strcat(strcat(out, var), paths[i].path);

+ return out;

+ }

+ }

+ return NULL;

+}