summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorErik K <erikk@previousplan.org>2022-05-13 17:24:26 +0000
committerErik K <erikk@previousplan.org>2022-05-13 17:24:26 +0000
commitb2fada1dd19211d71e04557653d08e697134a6ce (patch)
tree063b6d1280193046321db1e53506be5b72d2220f
initial commit
-rw-r--r--.gitignore6
-rw-r--r--Makefile24
-rw-r--r--config.h94
-rw-r--r--config.mk9
-rw-r--r--genplugin.awk20
-rw-r--r--posts.c93
-rw-r--r--rss.c84
-rw-r--r--skull.c82
-rw-r--r--sm.c442
-rw-r--r--sm.conf12
-rw-r--r--sm.h22
-rw-r--r--util.c83
-rw-r--r--util.h9
13 files changed, 980 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..74afe1c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+content
+export
+sm.tsv
+*.o
+sm
+plugins.h
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..2dd9aca
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,24 @@
+include config.mk
+SRC = sm.c util.c ${PLUGINS}
+OBJ = ${SRC:.c=.o}
+
+sm: ${OBJ}
+ ${CC} ${LDFLAGS} -o sm ${OBJ}
+
+.c.o:
+ ${CC} ${CFLAGS} -c $<
+
+${OBJ}: config.h plugins.h
+
+plugins.h: ${PLUGINS}
+ printf '%s\n' ${PLUGINS:.c=} | awk -f genplugin.awk
+
+install:
+ install -dm 0755 ${DESTDIR}${PREFIX}/bin
+ install -m 0755 sm ${DESTDIR}${PREFIX}/bin
+
+uninstall:
+ rm -f ${DESTDIR}${PREFIX}/bin/sm
+
+clean:
+ rm -f sm plugins.h ${OBJ}
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..96a42b7
--- /dev/null
+++ b/config.h
@@ -0,0 +1,94 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+#include <strings.h>
+
+#define CLEN 512
+
+#define DUMPSTR(N, S) fprintf(stderr, "%-15s: %s\n", N, S);
+#define DUMPCFG() \
+ DUMPSTR("DbLoc", kdbloc); \
+ DUMPSTR("ContentDir", kcontentdir); \
+ DUMPSTR("LinkPreset", klinkpreset); \
+ DUMPSTR("Autoedit", kautoedit); \
+ DUMPSTR("Autoexport", kautoexport); \
+ DUMPSTR("SkullExport", kskullexport); \
+ DUMPSTR("RssTitle", krsstitle); \
+ DUMPSTR("RssDescription", krssdescription); \
+ DUMPSTR("RssLink", krsslink); \
+ DUMPSTR("RssExport", krssexport); \
+ DUMPSTR("PostsExport", kpostsexport); \
+
+char *storepointer(const char *, int *);
+
+extern char kdbloc[];
+extern char kcontentdir[];
+extern char klinkpreset[];
+extern char kautoedit[];
+extern char kautoexport[];
+
+extern char kskullexport[];
+
+extern char krsstitle[];
+extern char krssdescription[];
+extern char krsslink[];
+extern char krssexport[];
+
+extern char kpostsexport[];
+
+#ifdef MAIN
+char kdbloc[CLEN] = "sm.tsv";
+char kcontentdir[CLEN] = "content";
+char klinkpreset[CLEN] = "https://previousplan.org/%";
+char kautoedit[CLEN] = "True";
+char kautoexport[CLEN] = "False";
+
+char kskullexport[CLEN] = "export";
+
+char krsstitle[CLEN] = "Previous Plan!";
+char krssdescription[CLEN] = "Previous Plan! Blog";
+char krsslink[CLEN] = "https://previousplan.org/rss.xml";
+char krssexport[CLEN] = "export/rss.xml";
+
+char kpostsexport[CLEN] = "export/posts.html";
+
+char *
+storepointer(const char *key, int *ispath)
+{
+ *ispath = 0;
+ if (!strcasecmp(key, "DbLoc")) {
+ *ispath = 1;
+ return kdbloc;
+ }
+ if (!strcasecmp(key, "ContentDir")) {
+ *ispath = 1;
+ return kcontentdir;
+ }
+ if (!strcasecmp(key, "LinkPreset"))
+ return klinkpreset;
+ if (!strcasecmp(key, "Autoedit"))
+ return kautoedit;
+ if (!strcasecmp(key, "Autoexport"))
+ return kautoexport;
+ if (!strcasecmp(key, "SkullExport")) {
+ *ispath = 1;
+ return kskullexport;
+ }
+ if (!strcasecmp(key, "RssTitle"))
+ return krsstitle;
+ if (!strcasecmp(key, "RssDescription"))
+ return krssdescription;
+ if (!strcasecmp(key, "RssLink"))
+ return krsslink;
+ if (!strcasecmp(key, "RssExport")) {
+ *ispath = 1;
+ return krssexport;
+ }
+ if (!strcasecmp(key, "PostsExport")) {
+ *ispath = 1;
+ return kpostsexport;
+ }
+ return NULL;
+}
+#endif
+
+#endif /* ! CONFIG_H */
diff --git a/config.mk b/config.mk
new file mode 100644
index 0000000..fe7e1b3
--- /dev/null
+++ b/config.mk
@@ -0,0 +1,9 @@
+PLUGINS = skull.c rss.c posts.c
+
+VERSION = 1.0
+PREFIX = /usr/local
+
+CPPFLAGS = -D_DEFAULT_SOURCE
+CFLAGS = ${CPPFLAGS} -O0 -g -DVERSION=\"${VERSION}\"
+# CFLAGS = ${CPPFLAGS} -O3 -pipe -march=native -DVERSION=\"${VERSION}\"
+LDFLAGS = -static
diff --git a/genplugin.awk b/genplugin.awk
new file mode 100644
index 0000000..e6f99ac
--- /dev/null
+++ b/genplugin.awk
@@ -0,0 +1,20 @@
+#!/usr/bin/awk -f
+
+BEGIN {
+ f = "plugins.h"
+ printf "#ifndef PLUGIN_H\n#define PLUGIN_H\n#include \"sm.h\"\n" >f
+}
+
+# We read the list of plugins from stdin
+{
+ plugins[i++] = $0
+ printf "extern Plugin %s;\n", $0 >f
+}
+
+END {
+ printf "#define NPLUGINS %d\n#ifdef MAIN\nPlugin *plugins[%d] = {\n", NR, NR >f
+ for (i in plugins) {
+ printf "\t&%s,\n", plugins[i] >f
+ }
+ printf "};\n#endif /* MAIN */\n\n#endif /* ! PLUGIN_H */" >f
+}
diff --git a/posts.c b/posts.c
new file mode 100644
index 0000000..239f118
--- /dev/null
+++ b/posts.c
@@ -0,0 +1,93 @@
+#include <stdlib.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "config.h"
+#include "sm.h"
+#include "util.h"
+
+extern char kcontentdir[];
+extern char kpostsexport[];
+
+static void postsexport(Document **, int);
+
+Plugin posts = {"posts", postsexport, NULL, 0};
+
+static int
+creatcompar(const Document **d1, const Document **d2)
+{
+ return (*d2)->creat - (*d1)->creat;
+}
+
+static void
+postsexport(Document **doc, int ndoc)
+{
+ struct tm *tm, lasttm;
+ char dateheader[64];
+ FILE *outf;
+ int i, j;
+
+ static char header[] = "\
+<!DOCTYPE html>\n\
+<html>\n\
+ <title>Posts</title>\n\
+ <meta charset=\"utf-8\"/>\n\
+ <style>\n\
+ h1, nav { text-align: center }\n\
+ p { margin-left: 6%; margin-right: 6% }\n\
+ </style>\n\
+</head>\n\
+<body bgcolor=\"#000000\" background=\"/pix/crackblk.jpg\" text=\"#eeeeee\" \
+link=\"orange\" alink=\"red\" vlink=\"red\">\n\
+ <a href=\"https://previousplan.org\"><img src=\"/pix/previousplan-button.gif\" alt=\"PreviousPlan! web button\" title=\"PreviousPlan! web button\"></a>\n\
+ <nav>\n\
+ <img src=\"/pix/s-spin.gif\"/>\n\
+ <i><font size=\"6\" color=\"gold\">Previous Plan!</font></i>\n\
+ -\n\
+ <a href=\"/about.html\">Home</a>\n\
+ -\n\
+ <a href=\"/posts.html\">Posts</a>\n\
+ -\n\
+ <a href=\"/rss.xml\">RSS</a>\n\
+ -\n\
+ <a href=\"/donate.html\">Donate</a>\n\
+ <img src=\"/pix/s-spin.gif\"/>\n\
+ </nav>\n\
+ <h2>List of posts</h2>\n\
+";
+
+ static char footer[] = "\
+ <hr/>\n\
+ <img src=\"pix/s-mail.gif\" alt=\"EMAIL\"/>\n\
+ <p>Webmaster email: <a \
+href=\"mailto:erikk@previousplan.org\">erikk@previousplan.org</a> \
+<a href=\"/erikk.asc\">(PGP key)</a></p>\n\
+</body>\n\
+</html>\n\
+";
+
+ char title[128], link[CLEN];
+ char *item[] = { " <li><a href=\"", link, "\">", title, "</a></li>\n" };
+
+ qsort(doc, ndoc, sizeof(*doc), (int (*)(const void *, const void *))creatcompar);
+
+ outf = xfopen(kpostsexport, "w");
+ xfputs(header, outf);
+ for (i = 0; i < ndoc; i++) {
+ my_strlcpy(title, doc[i]->title, sizeof(title));
+ my_strlcpy(link, doc[i]->link, sizeof(link));
+
+ tm = gmtime(&doc[i]->creat);
+ if (i == 0 || tm->tm_mon != lasttm.tm_mon || tm->tm_year != lasttm.tm_year) {
+ strftime(dateheader, sizeof(dateheader), i != 0 ? "</ul>\n<h3>%B %Y</h3>\n<ul>\n" : "<h3>%B %Y</h3>\n<ul>\n", tm);
+ xfputs(dateheader, outf);
+ }
+ memcpy(&lasttm, tm, sizeof(lasttm));
+ for (j = 0; j < LEN(item); j++)
+ xfputs(item[j], outf);
+ }
+ if (ndoc)
+ xfputs("</ul>\n", outf);
+ xfputs(footer, outf);
+ fclose(outf);
+}
diff --git a/rss.c b/rss.c
new file mode 100644
index 0000000..e0f22fe
--- /dev/null
+++ b/rss.c
@@ -0,0 +1,84 @@
+#include <stdlib.h>
+
+#include "config.h"
+#include "sm.h"
+#include "util.h"
+
+extern char kcontentdir[];
+extern char krssexport[];
+extern char krsstitle[];
+extern char krssdescription[];
+extern char krsslink[];
+
+static void rssexport(Document **, int);
+
+Plugin rss = {"rss", rssexport, NULL, 0};
+
+static int
+creatcompar(const Document **d1, const Document **d2)
+{
+ return (*d2)->creat - (*d1)->creat;
+}
+
+static void
+rssexport(Document **doc, int ndoc)
+{
+ char inpath[512];
+ FILE *outf;
+ int i, j;
+
+ char *header[] = { "\
+<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
+<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n\
+\n\
+<channel>\n\
+<title>", krsstitle, "</title>\n\
+<description>", krssdescription, "</description>\n\
+<language>en-us</language>\n\
+<link>", krsslink, "</link>\n\
+<generator>sm-"VERSION"</generator>\n\
+<atom:link href=\"", krsslink, "\" rel=\"self\" type=\"application/rss+xml\" />\n\
+" };
+
+ char *footer[] = { "\
+</channel>\n\
+</rss>\n\
+" };
+
+ char filename[64], title[128], creat[64], link[CLEN];
+ char *itemheader[] = { "\
+<item>\n\
+ <title>", title, "</title>\n\
+ <guid isPermaLink=\"false\">", filename, "</guid>\n\
+ <pubDate>", creat, "</pubDate>\n\
+ <link>", link, "</link>\n\
+ <description><![CDATA[" };
+
+ char *itemfooter[] = { "\
+]]></description>\n\
+</item>\n\
+" };
+
+ qsort(doc, ndoc, sizeof(*doc), (int (*)(const void *, const void *))creatcompar);
+
+ outf = xfopen(krssexport, "w");
+ for (j = 0; j < LEN(header); j++)
+ xfputs(header[j], outf);
+ for (i = 0; i < ndoc; i++) {
+ snprintf(inpath, sizeof(inpath), "%s/%s", kcontentdir,
+ doc[i]->filename);
+ my_strlcpy(filename, doc[i]->filename, sizeof(filename));
+ my_strlcpy(title, doc[i]->title, sizeof(title));
+ strftime(creat, sizeof(creat), "%a, %d %b %Y %H:%M:%S %z",
+ gmtime(&doc[i]->creat));
+ my_strlcpy(link, doc[i]->link, sizeof(link));
+ for (j = 0; j < LEN(itemheader); j++)
+ xfputs(itemheader[j], outf);
+ cat(inpath, outf);
+ for (j = 0; j < LEN(itemfooter); j++)
+ xfputs(itemfooter[j], outf);
+ }
+ for (j = 0; j < LEN(footer); j++)
+ xfputs(footer[j], outf);
+ fclose(outf);
+}
diff --git a/skull.c b/skull.c
new file mode 100644
index 0000000..5c44f20
--- /dev/null
+++ b/skull.c
@@ -0,0 +1,82 @@
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+
+#include "sm.h"
+#include "util.h"
+
+extern char kcontentdir[];
+extern char kskullexport[];
+
+static void skullexport(Document **, int);
+
+Plugin skull = {"skull", skullexport, NULL, 0};
+
+static void
+skullexport(Document **doc, int ndoc)
+{
+ char path[512], outpath[512];
+ FILE *outf;
+ int i, j;
+
+ char title[128], creat[64], mod[64];
+ char *header[] = { "\
+<!DOCTYPE html>\n\
+<html>\n\
+ <title>", title, "</title>\n\
+ <meta charset=\"utf-8\"/>\n\
+ <style>\n\
+ h1, nav { text-align: center }\n\
+ p { margin-left: 6%; margin-right: 6% }\n\
+ </style>\n\
+</head>\n\
+<body bgcolor=\"#000000\" background=\"/pix/crackblk.jpg\" text=\"#eeeeee\" \
+link=\"orange\" alink=\"red\" vlink=\"red\">\n\
+ <a href=\"https://previousplan.org\"><img src=\"/pix/previousplan-button.gif\" alt=\"PreviousPlan! web button\" title=\"PreviousPlan! web button\"></a>\n\
+ <nav>\n\
+ <img src=\"/pix/s-spin.gif\"/>\n\
+ <i><font size=\"6\" color=\"gold\">Previous Plan!</font></i>\n\
+ -\n\
+ <a href=\"/about.html\">Home</a>\n\
+ -\n\
+ <a href=\"/posts.html\">Posts</a>\n\
+ -\n\
+ <a href=\"/rss.xml\">RSS</a>\n\
+ -\n\
+ <a href=\"/donate.html\">Donate</a>\n\
+ <img src=\"/pix/s-spin.gif\"/>\n\
+ </nav>\n\
+" };
+
+ char *footer[] = { "\
+ <hr/>\n\
+ <p>Written: ", creat, ", Last modified: ", mod, "</p>\n\
+ <img src=\"pix/s-mail.gif\" alt=\"EMAIL\"/>\n\
+ <p>Webmaster email: <a \
+href=\"mailto:erikk@previousplan.org\">erikk@previousplan.org</a> \
+<a href=\"/erikk.asc\">(PGP key)</a></p>\n\
+</body>\n\
+</html>\n\
+" };
+
+ for (i = 0; i < ndoc; i++) {
+ snprintf(path, sizeof(path), "%s/%s", kcontentdir, doc[i]->filename);
+ snprintf(outpath, sizeof(outpath), "%s/%s", kskullexport, doc[i]->filename);
+
+ if ((outf = fopen(outpath, "w")) == NULL) {
+ fprintf(stderr, "%s: %s\n", outpath, strerror(errno));
+ continue;
+ }
+
+ my_strlcpy(title, doc[i]->title, sizeof(title));
+ strftime(creat, sizeof(creat), "%a, %Y %b %d", gmtime(&doc[i]->creat));
+ strftime(mod, sizeof(mod), "%a, %Y %b %d", gmtime(&doc[i]->mod));
+ for (j = 0; j < LEN(header); j++)
+ xfputs(header[j], outf);
+ cat(path, outf);
+ for (j = 0; j < LEN(footer); j++)
+ xfputs(footer[j], outf);
+ fclose(outf);
+ }
+}
diff --git a/sm.c b/sm.c
new file mode 100644
index 0000000..d4064c7
--- /dev/null
+++ b/sm.c
@@ -0,0 +1,442 @@
+#include <sys/wait.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "util.h"
+
+#define MAIN
+#include "config.h"
+#include "plugins.h"
+
+#define MIN(A, B) (A < B ? A : B)
+
+typedef struct {
+ char argv[10][CLEN];
+ int argc;
+} Cmd;
+
+int cat(const char *, FILE *);
+
+static int dbdel(const char *);
+static Document *dbfind(const char *);
+static void dbread(const char *);
+static void dbwrite(const char *);
+static void doautoexport(const char *);
+static void cmdsplit(Cmd *, char *);
+static void cfgread(const char *);
+static void expandlink(Document *d);
+static void expandpath(char *, size_t);
+static int pladddoc(const char *, Document *);
+
+static int add(const char *);
+static int edit(const char *);
+static void list(const char *);
+
+static Document **docdb;
+static int ndocdb;
+static int autoedit;
+static int autoexport;
+
+int
+cat(const char *in, FILE *outf)
+{
+ char buf[BUFSIZ];
+ size_t ret;
+ FILE *inf;
+
+ if ((inf = fopen(in, "r")) == NULL) {
+ fprintf(stderr, "%s: %s\n", in, strerror(errno));
+ return 0;
+ }
+ while ((ret = fread(buf, 1, sizeof(buf), inf))) {
+ if (!fwrite(buf, ret, 1, outf)) {
+ fprintf(stderr, "fwrite: %s\n", strerror(errno));
+ exit(1);
+ }
+ }
+ if (ferror(inf)) {
+ fprintf(stderr, "fread: %s\n", strerror(errno));
+ exit(1);
+ }
+ return 1;
+}
+
+
+static int
+dbdel(const char *filename)
+{
+ char path[512];
+ Document *d;
+ int i, j;
+ int di;
+
+ if ((d = dbfind(filename)) == NULL) {
+ fprintf(stderr, "%s: no such file\n", filename);
+ return 0;
+ }
+
+ for (i = 0; i < NPLUGINS; i++) {
+ for (j = 0; j < plugins[i]->nusedby; j++) {
+ if (plugins[i]->usedby[j] != d)
+ continue;
+ if (j + 1 < plugins[i]->nusedby) {
+ memmove(&plugins[i]->usedby[j], &plugins[i]->usedby[j +
+ 1], (plugins[i]->nusedby - j - 1) *
+ sizeof(*plugins[i]->usedby));
+ }
+ plugins[i]->usedby = xrealloc(plugins[i]->usedby,
+ --plugins[i]->nusedby *
+ sizeof(*plugins[i]->usedby));
+ }
+ }
+
+ for (di = 0; di < ndocdb; di++) {
+ if (docdb[di] == d)
+ break;
+ }
+ if (di == ndocdb)
+ return 0;
+ if (di + 1 < ndocdb) {
+ memmove(&docdb[di], &docdb[di + 1], (ndocdb - di - 1) *
+ sizeof(*docdb));
+ }
+ docdb = xrealloc(docdb, --ndocdb * sizeof(*docdb));
+
+ snprintf(path, sizeof(path), "%s/%s", kcontentdir, filename);
+ unlink(path);
+ free(d);
+ return 1;
+}
+
+static Document *
+dbfind(const char *filename)
+{
+ int i;
+
+ for (i = 0; i < ndocdb; i++) {
+ if (!strcmp(docdb[i]->filename, filename)) {
+ return docdb[i];
+ }
+ }
+
+ return NULL;
+}
+
+static void
+dbread(const char *path)
+{
+ FILE *db;
+ char buf[1024], *field, *p;
+ Document *d;
+ int i;
+
+ if ((db = fopen(path, "r")) == NULL)
+ return;
+ while (xfgets(buf, sizeof(buf), db)) {
+ if (buf[0] == '\0')
+ continue;
+ d = xmalloc(sizeof(*d));
+ for (i = 0, p = buf; (field = strsep(&p, "\t")); i++) {
+ switch (i) {
+ case 0: my_strlcpy(d->filename, field, sizeof(d->filename)); break;
+ case 1: my_strlcpy(d->title, field, sizeof(d->title)); break;
+ case 2: d->creat = strtoll(field, NULL, 10); break;
+ case 3: d->mod = strtoll(field, NULL, 10); break;
+ default: pladddoc(field, d);
+ }
+ }
+ if (i > 3) {
+ expandlink(d);
+ docdb = xrealloc(docdb, ++ndocdb * sizeof(*docdb));
+ docdb[ndocdb-1] = d;
+ } else {
+ free(d);
+ }
+ }
+ fclose(db);
+}
+
+static void
+dbwrite(const char *path)
+{
+ FILE *db;
+ int i, j, k;
+
+ db = xfopen(path, "w");
+ for (i = 0; i < ndocdb; i++) {
+ fprintf(db, "%s\t%s\t%ld\t%ld", docdb[i]->filename,
+ docdb[i]->title, docdb[i]->creat,
+ docdb[i]->mod);
+ for (j = 0; j < NPLUGINS; j++) {
+ for (k = 0; k < plugins[j]->nusedby; k++) {
+ if (plugins[j]->usedby[k] == docdb[i]) {
+ fprintf(db, "\t%s", plugins[j]->name);
+ break;
+ }
+ }
+ }
+ fputs("\n", db);
+ }
+ fclose(db);
+}
+
+static void
+doautoexport(const char *filename)
+{
+ Document *d;
+ int i, j;
+
+ if ((d = dbfind(filename)) == NULL)
+ return;
+ for (i = 0; i < NPLUGINS; i++) {
+ for (j = 0; j < plugins[i]->nusedby; j++) {
+ if (plugins[i]->usedby[j] == d) {
+ plugins[i]->export(plugins[i]->usedby,
+ plugins[i]->nusedby);
+ }
+ }
+ }
+}
+
+static void
+cmdsplit(Cmd *cmd, char *line)
+{
+ char *p, *arg;
+
+ p = line;
+ cmd->argc = 0;
+ while (cmd->argc < LEN(cmd->argv) && (arg = strsep(&p, " "))) {
+ if (!arg[0])
+ continue;
+ my_strlcpy(cmd->argv[cmd->argc++], arg, sizeof(cmd->argv[cmd->argc]));
+ }
+}
+
+static void
+cfgread(const char *path)
+{
+ char buf[CLEN], *end, *value, *store;
+ int ispath;
+ FILE *cfg;
+
+ cfg = xfopen(path, "r");
+ while (xfgets(buf, sizeof(buf), cfg)) {
+ if (buf[0] == '#' || !buf[0])
+ continue;
+ if ((value = strchr(buf, ' ')) == NULL || value == buf)
+ continue;
+ *value++ = '\0';
+ for (end = value + strlen(value) - 1; end >= value
+ && isspace(*end); *end-- = '\0') {
+ continue;
+ }
+ if ((store = storepointer(buf, &ispath))) {
+ my_strlcpy(store, value, CLEN);
+ if (ispath)
+ expandpath(store, CLEN);
+ }
+ }
+ fclose(cfg);
+}
+
+static void
+expandlink(Document *d)
+{
+ int i, j;
+
+ for (i = j = 0; j + 1 < sizeof(d->link) && klinkpreset[i]; i++) {
+ if (klinkpreset[i] == '%') {
+ my_strlcpy(&d->link[j], d->filename, sizeof(d->link) - j);
+ j += MIN(sizeof(d->link) - j - 1, strlen(d->filename));
+ } else {
+ d->link[j++] = klinkpreset[i];
+ }
+ }
+ d->link[j] = '\0';
+}
+
+static void
+expandpath(char *str, size_t max)
+{
+ const char *home = getenv("HOME");
+ char newpath[CLEN];
+
+ if (home == NULL)
+ return;
+ if (str[0] == '~') {
+ snprintf(newpath, sizeof(newpath), "%s%s", home, str + 1);
+ my_strlcpy(str, newpath, max);
+ }
+}
+
+/* pladddoc -- e.g -- pluginadddoc */
+static int
+pladddoc(const char *plname, Document *d)
+{
+ int i, j;
+
+ for (i = 0; i < NPLUGINS; i++) {
+ if (strcmp(plugins[i]->name, plname) != 0)
+ continue;
+ for (j = 0; j < plugins[i]->nusedby; j++) {
+ if (plugins[i]->usedby[j] == d)
+ goto next;
+ }
+ plugins[i]->usedby = xrealloc(plugins[i]->usedby,
+ ++plugins[i]->nusedby *
+ sizeof(*plugins[i]->usedby));
+ plugins[i]->usedby[plugins[i]->nusedby-1] = d;
+next:
+ }
+}
+
+
+static int
+add(const char *filename)
+{
+ Document *d;
+ char title[128];
+
+ if ((d = dbfind(filename))) {
+ fprintf(stderr, "%s: file exists\n", filename);
+ return 0;
+ }
+
+ xfputs("Title: ", stdout);
+ fflush(stdout);
+ if (!xfgets(title, sizeof(title), stdin))
+ return 0;
+
+ d = xmalloc(sizeof(*d));
+ my_strlcpy(d->filename, filename, sizeof(d->filename));
+ my_strlcpy(d->title, title, sizeof(d->title));
+ d->creat = d->mod = time(NULL);
+
+ expandlink(d);
+ docdb = xrealloc(docdb, ++ndocdb * sizeof(*docdb));
+ docdb[ndocdb-1] = d;
+
+ return 1;
+}
+
+static int
+edit(const char *filename)
+{
+ Document *d;
+ char path[512];
+ pid_t pid;
+
+ if ((d = dbfind(filename)) == NULL) {
+ fprintf(stderr, "%s: no such file\n", filename);
+ return 0;
+ }
+ snprintf(path, sizeof(path), "%s/%s", kcontentdir, filename);
+ switch ((pid = fork())) {
+ case -1:
+ fprintf(stderr, "fork: %s\n", strerror(errno));
+ exit(1);
+ case 0:
+ execl("/bin/sh", "sh", "-c", "$EDITOR \"$1\"", "sm-edit", path, NULL);
+ exit(1);
+ }
+ waitpid(pid, NULL, 0);
+ d->mod = time(NULL);
+ return 1;
+}
+
+static void
+list(const char *filename)
+{
+ char path[512];
+ FILE *f;
+ int i;
+
+ if (filename) {
+ snprintf(path, sizeof(path), "%s/%s", kcontentdir, filename);
+ cat(path, stdout);
+ } else for (i = 0; i < ndocdb; i++) {
+ printf("%-15s: %s\n", docdb[i]->filename, docdb[i]->title);
+ }
+}
+
+
+
+int
+main(int argc, char *argv[])
+{
+ char *cfgpath = "sm.conf";
+ char linebuf[CLEN];
+ Document *d;
+ Cmd cmd;
+ int i;
+
+ if (argc >= 2)
+ cfgpath = argv[1];
+
+ cfgread(cfgpath);
+ DUMPCFG();
+
+ if (!strcasecmp(kautoedit, "yes") || !strcasecmp(kautoedit, "true"))
+ autoedit = 1;
+ if (!strcasecmp(kautoexport, "yes") || !strcasecmp(kautoexport, "true"))
+ autoexport = 1;
+
+ dbread(kdbloc);
+ for (;;) {
+ xfputs("[sm-" VERSION "] ", stdout);
+ fflush(stdout);
+ if (!xfgets(linebuf, sizeof(linebuf), stdin))
+ break;
+ cmdsplit(&cmd, linebuf);
+ if (cmd.argc == 0)
+ continue;
+ for (i = 0; i < NPLUGINS; i++) {
+ if (!strcmp(plugins[i]->name, cmd.argv[0])) {
+ plugins[i]->export(plugins[i]->usedby, plugins[i]->nusedby);
+ break;
+ }
+ }
+ if (i != NPLUGINS)
+ continue;
+ if (!strcmp(cmd.argv[0], "quit") || !strcmp(cmd.argv[0], "exit"))
+ break;
+ if (!strcmp(cmd.argv[0], "list")) {
+ list(cmd.argc >= 2 ? cmd.argv[1] : NULL);
+ } else if (!strcmp(cmd.argv[0], "save")) {
+ dbwrite(kdbloc);
+ } else if (cmd.argc < 2) {
+ fprintf(stderr, "invalid command/needs more parameters\n");
+ } else if (!strcmp(cmd.argv[0], "add")) {
+ if (add(cmd.argv[1]))
+ dbwrite(kdbloc);
+ if (autoedit)
+ goto editcmd;
+ } else if (!strcmp(cmd.argv[0], "edit")) {
+editcmd:
+ if (edit(cmd.argv[1])) {
+ doautoexport(cmd.argv[1]);
+ dbwrite(kdbloc);
+ }
+ } else if (!strcmp(cmd.argv[0], "rm")) {
+ if (dbdel(cmd.argv[1]))
+ dbwrite(kdbloc);
+ } else if (cmd.argc < 3) {
+ fprintf(stderr, "invalid command/needs more parameters\n");
+ } else if (!strcmp(cmd.argv[0], "set")) {
+ if ((d = dbfind(cmd.argv[1])) == NULL) {
+ fprintf(stderr, "%s: no such file\n", cmd.argv[1]);
+ continue;
+ }
+ if (pladddoc(cmd.argv[2], d))
+ dbwrite(kdbloc);
+ }
+ }
+ fputc('\n', stdout);
+ return 0;
+}
diff --git a/sm.conf b/sm.conf
new file mode 100644
index 0000000..63a1352
--- /dev/null
+++ b/sm.conf
@@ -0,0 +1,12 @@
+DbLoc ~/sites/sm/previousplan.org.tsv
+ContentDir ~/sites/sm/previousplan.org
+LinkPreset https://previousplan.org/%
+Autoedit True
+Autoexport True
+
+SkullExport ~/sites/previousplan.org
+RssTitle Previous Plan!
+RssDescription Previous Plan! Blog
+RssLink https://previousplan.org/rss.xml
+RssExport ~/sites/previousplan.org/rss.xml
+PostsExport ~/sites/previousplan.org/posts.html
diff --git a/sm.h b/sm.h
new file mode 100644
index 0000000..085b92f
--- /dev/null
+++ b/sm.h
@@ -0,0 +1,22 @@
+#include <stdio.h>
+#include <time.h>
+
+#include "config.h"
+
+#define LEN(X) (sizeof(X)/sizeof(*X))
+
+typedef struct {
+ char filename[64];
+ char title[128];
+ time_t creat, mod;
+ char link[CLEN];
+} Document;
+
+typedef struct {
+ const char name[16];
+ void (*export)(Document **, int);
+ Document **usedby;
+ int nusedby;
+} Plugin;
+
+int cat(const char *, FILE *);
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..3f4f51d
--- /dev/null
+++ b/util.c
@@ -0,0 +1,83 @@
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "util.h"
+
+/* This isn't a 1:1 wrapper around fgets. It also removes that annoying
+ * trailing \n automatically */
+char *
+xfgets(char *s, size_t size, FILE *stream)
+{
+ char *p;
+ if ((s = fgets(s, size, stream))) {
+ if ((p = strchr(s, '\n')))
+ *p = '\0';
+ } else if (ferror(stream)) {
+ fprintf(stderr, "fgets: %s\n", strerror(errno));
+ exit(1);
+ }
+ return s;
+}
+
+FILE *
+xfopen(const char *pathname, const char *mode)
+{
+ FILE *f;
+ if ((f = fopen(pathname, mode)) == NULL) {
+ fprintf(stderr, "%s: %s\n", pathname, strerror(errno));
+ exit(1);
+ }
+ return f;
+}
+
+int
+xfputs(const char *s, FILE *stream)
+{
+ int d;
+ if ((d = fputs(s, stream)) == EOF) {
+ fprintf(stderr, "fputs: %s\n", strerror(errno));
+ exit(1);
+ }
+ return d;
+}
+
+void *
+xmalloc(size_t size)
+{
+ void *n;
+ if ((n = malloc(size)) == NULL) {
+ fprintf(stderr, "malloc: %s\n", strerror(errno));
+ exit(1);
+ }
+ return n;
+}
+
+void *
+xrealloc(void *ptr, size_t size)
+{
+ void *n;
+ if ((n = realloc(ptr, size)) == NULL) {
+ fprintf(stderr, "malloc: %s\n", strerror(errno));
+ exit(1);
+ }
+ return n;
+}
+
+/* strlcpy is unportable, and strncpy is a mess. So we define our own strlcpy
+ * instead. */
+size_t
+my_strlcpy(char *dst, const char *src, size_t size)
+{
+ const char *p2;
+ char *p1, *stop;
+
+ p1 = dst;
+ p2 = src;
+ stop = dst + size;
+ while (*p2 && p1 != stop)
+ *p1++ = *p2++;
+ *p1 = '\0';
+ return (size_t)(p1 - dst);
+}
diff --git a/util.h b/util.h
new file mode 100644
index 0000000..e1e49e7
--- /dev/null
+++ b/util.h
@@ -0,0 +1,9 @@
+#include <stdio.h>
+#include <stdlib.h>
+
+char *xfgets(char *, size_t, FILE *);
+FILE *xfopen(const char *, const char *);
+int xfputs(const char *, FILE *);
+void *xmalloc(size_t);
+void *xrealloc(void *, size_t);
+size_t my_strlcpy(char *, const char *, size_t);