summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--catgirl.127
-rw-r--r--chat.c17
-rw-r--r--chat.h4
-rw-r--r--config.c8
-rw-r--r--ui.c176
5 files changed, 220 insertions, 12 deletions
diff --git a/catgirl.1 b/catgirl.1
index 15b387b..00f875b 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -20,6 +20,7 @@
 .Op Fl n Ar nick
 .Op Fl p Ar port
 .Op Fl r Ar real
+.Op Fl s Ar save
 .Op Fl u Ar user
 .Op Fl w Ar pass
 .Op Ar config ...
@@ -123,6 +124,18 @@ Set realname to
 .Ar real .
 The default realname is the same as the nickname.
 .
+.It Fl s Ar name , Cm save = Ar name
+Load and save the contents of windows from
+.Ar name
+in
+.Pa $XDG_DATA_DIRS/catgirl ,
+or an absolute or relative path if
+.Ar name
+starts with
+.Ql /
+or
+.Ql \&. .
+.
 .It Fl u Ar user , Cm user = Ar user
 Set username to
 .Ar user .
@@ -324,7 +337,7 @@ The color numbers are as follows:
 .Sh FILES
 .Bl -tag -width Ds
 .It Pa $XDG_CONFIG_DIRS/catgirl
-Configuration files are search for first in
+Configuration files are searched for first in
 .Ev $XDG_CONFIG_HOME ,
 usually
 .Pa ~/.config ,
@@ -334,6 +347,18 @@ usually
 .Pa /etc/xdg .
 .It Pa ~/.config/catgirl
 The most likely location of configuration files.
+.
+.It Pa $XDG_DATA_DIRS/catgirl
+Save files are searched for first in
+.Ev $XDG_DATA_HOME ,
+usually
+.Pa ~/.local/share ,
+followed by the colon-separated list of paths
+.Ev $XDG_DATA_DIRS ,
+usually
+.Pa /usr/local/share:/usr/share .
+.It Pa ~/.local/share/catgirl
+The most likely location of save files.
 .El
 .
 .Sh EXAMPLES
diff --git a/chat.c b/chat.c
index c58fdc5..e8713bb 100644
--- a/chat.c
+++ b/chat.c
@@ -47,6 +47,15 @@ size_t idNext = Network + 1;
 
 struct Self self = { .color = Default };
 
+static const char *save;
+static void exitSave(void) {
+	int error = uiSave(save);
+	if (error) {
+		warn("%s", save);
+		_exit(EX_IOERR);
+	}
+}
+
 uint32_t hashInit;
 
 int procPipe[2] = { -1, -1 };
@@ -84,7 +93,7 @@ int main(int argc, char *argv[]) {
 	const char *user = NULL;
 	const char *real = NULL;
 
-	const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:u:vw:";
+	const char *Opts = "!C:H:O:a:c:eh:j:k:n:p:r:s:u:vw:";
 	const struct option LongOpts[] = {
 		{ "insecure", no_argument, NULL, '!' },
 		{ "copy", required_argument, NULL, 'C' },
@@ -99,6 +108,7 @@ int main(int argc, char *argv[]) {
 		{ "nick", required_argument, NULL, 'n' },
 		{ "port", required_argument, NULL, 'p' },
 		{ "real", required_argument, NULL, 'r' },
+		{ "save", required_argument, NULL, 's' },
 		{ "user", required_argument, NULL, 'u' },
 		{ "debug", no_argument, NULL, 'v' },
 		{ "pass", required_argument, NULL, 'w' },
@@ -121,6 +131,7 @@ int main(int argc, char *argv[]) {
 			break; case 'n': nick = optarg;
 			break; case 'p': port = optarg;
 			break; case 'r': real = optarg;
+			break; case 's': save = optarg;
 			break; case 'u': user = optarg;
 			break; case 'v': self.debug = true;
 			break; case 'w': pass = optarg;
@@ -154,6 +165,10 @@ int main(int argc, char *argv[]) {
 	if (privFile) fclose(privFile);
 
 	uiInit();
+	if (save) {
+		uiLoad(save);
+		atexit(exitSave);
+	}
 	uiShowID(Network);
 	uiFormat(Network, Cold, NULL, "Traveling...");
 	uiDraw();
diff --git a/chat.h b/chat.h
index 13319da..47a6163 100644
--- a/chat.h
+++ b/chat.h
@@ -26,6 +26,8 @@
 #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
 #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
 
+#define XDG_SUBDIR "catgirl"
+
 typedef unsigned char byte;
 
 int procPipe[2];
@@ -144,6 +146,8 @@ void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str);
 void uiFormat(
 	size_t id, enum Heat heat, const time_t *time, const char *format, ...
 ) __attribute__((format(printf, 4, 5)));
+void uiLoad(const char *name);
+int uiSave(const char *name);
 
 enum Edit {
 	EditHead,
diff --git a/config.c b/config.c
index b3e42f9..3bf56c0 100644
--- a/config.c
+++ b/config.c
@@ -24,8 +24,6 @@
 
 #include "chat.h"
 
-#define CONFIG_DIR "catgirl"
-
 FILE *configOpen(const char *path, const char *mode) {
 	if (path[0] == '/' || path[0] == '.') goto local;
 
@@ -35,10 +33,10 @@ FILE *configOpen(const char *path, const char *mode) {
 
 	char buf[PATH_MAX];
 	if (configHome) {
-		snprintf(buf, sizeof(buf), "%s/" CONFIG_DIR "/%s", configHome, path);
+		snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path);
 	} else {
 		if (!home) goto local;
-		snprintf(buf, sizeof(buf), "%s/.config/" CONFIG_DIR "/%s", home, path);
+		snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path);
 	}
 	FILE *file = fopen(buf, mode);
 	if (file) return file;
@@ -48,7 +46,7 @@ FILE *configOpen(const char *path, const char *mode) {
 	while (*configDirs) {
 		size_t len = strcspn(configDirs, ":");
 		snprintf(
-			buf, sizeof(buf), "%.*s/" CONFIG_DIR "/%s",
+			buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s",
 			(int)len, configDirs, path
 		);
 		file = fopen(buf, mode);
diff --git a/ui.c b/ui.c
index 9a070a6..57ef322 100644
--- a/ui.c
+++ b/ui.c
@@ -20,11 +20,14 @@
 #include <ctype.h>
 #include <curses.h>
 #include <err.h>
+#include <errno.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
 #include <sysexits.h>
 #include <term.h>
 #include <termios.h>
@@ -66,6 +69,13 @@ static void bufferPush(struct Buffer *buffer, time_t time, const char *line) {
 	if (!buffer->lines[i]) err(EX_OSERR, "strdup");
 }
 
+static time_t bufferTime(const struct Buffer *buffer, size_t i) {
+	return buffer->times[(buffer->len + i) % BufferCap];
+}
+static const char *bufferLine(const struct Buffer *buffer, size_t i) {
+	return buffer->lines[(buffer->len + i) % BufferCap];
+}
+
 enum { WindowLines = BufferCap };
 struct Window {
 	size_t id;
@@ -532,9 +542,8 @@ static void reflow(struct Window *window) {
 	werase(window->pad);
 	wmove(window->pad, WindowLines - 1, 0);
 	window->unreadLines = 0;
-	struct Buffer *buffer = &window->buffer;
 	for (size_t i = 0; i < BufferCap; ++i) {
-		char *line = buffer->lines[(buffer->len + i) % BufferCap];
+		const char *line = bufferLine(&window->buffer, i);
 		if (!line) continue;
 		waddch(window->pad, '\n');
 		if (i >= (size_t)(BufferCap - window->unreadCount)) {
@@ -557,12 +566,12 @@ static void resize(void) {
 	statusUpdate();
 }
 
-static void bufferList(struct Buffer *buffer) {
+static void bufferList(const struct Buffer *buffer) {
 	uiHide();
 	waiting = true;
 	for (size_t i = 0; i < BufferCap; ++i) {
-		time_t time = buffer->times[(buffer->len + i) % BufferCap];
-		const char *line = buffer->lines[(buffer->len + i) % BufferCap];
+		time_t time = bufferTime(buffer, i);
+		const char *line = bufferLine(buffer, i);
 		if (!line) continue;
 
 		struct tm *tm = localtime(&time);
@@ -848,3 +857,160 @@ void uiRead(void) {
 	}
 	inputUpdate();
 }
+
+static FILE *dataOpen(const char *path, const char *mode) {
+	if (path[0] == '/' || path[0] == '.') goto local;
+
+	const char *home = getenv("HOME");
+	const char *dataHome = getenv("XDG_DATA_HOME");
+	const char *dataDirs = getenv("XDG_DATA_DIRS");
+
+	char homePath[PATH_MAX];
+	if (dataHome) {
+		snprintf(
+			homePath, sizeof(homePath),
+			"%s/" XDG_SUBDIR "/%s", dataHome, path
+		);
+	} else {
+		if (!home) goto local;
+		snprintf(
+			homePath, sizeof(homePath),
+			"%s/.local/share/" XDG_SUBDIR "/%s", home, path
+		);
+	}
+	FILE *file = fopen(homePath, mode);
+	if (file) return file;
+	if (errno != ENOENT) {
+		warn("%s", homePath);
+		return NULL;
+	}
+
+	char buf[PATH_MAX];
+	if (!dataDirs) dataDirs = "/usr/local/share:/usr/share";
+	while (*dataDirs) {
+		size_t len = strcspn(dataDirs, ":");
+		snprintf(
+			buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s",
+			(int)len, dataDirs, path
+		);
+		file = fopen(buf, mode);
+		if (file) return file;
+		if (errno != ENOENT) {
+			warn("%s", buf);
+			return NULL;
+		}
+		dataDirs += len;
+		if (*dataDirs) dataDirs++;
+	}
+
+	if (mode[0] != 'r') {
+		char *base = strrchr(homePath, '/');
+		*base = '\0';
+		int error = mkdir(homePath, S_IRWXU);
+		if (error && errno != EEXIST) {
+			warn("%s", homePath);
+			return NULL;
+		}
+		*base = '/';
+		file = fopen(homePath, mode);
+		if (!file) warn("%s", homePath);
+		return file;
+	}
+
+local:
+	file = fopen(path, mode);
+	if (!file) warn("%s", path);
+	return file;
+}
+
+static const size_t Signatures[] = {
+	0x6C72696774616301,
+};
+
+static size_t signatureVersion(size_t signature) {
+	for (size_t i = 0; i < ARRAY_LEN(Signatures); ++i) {
+		if (signature == Signatures[i]) return i;
+	}
+	err(EX_DATAERR, "unknown file signature %zX", signature);
+}
+
+static int writeSize(FILE *file, size_t value) {
+	return (fwrite(&value, sizeof(value), 1, file) ? 0 : -1);
+}
+static int writeTime(FILE *file, time_t time) {
+	return (fwrite(&time, sizeof(time), 1, file) ? 0 : -1);
+}
+static int writeString(FILE *file, const char *str) {
+	return (fwrite(str, strlen(str) + 1, 1, file) ? 0 : -1);
+}
+
+int uiSave(const char *name) {
+	FILE *file = dataOpen(name, "w");
+	if (!file) return -1;
+
+	if (writeSize(file, Signatures[0])) return -1;
+	const struct Window *window;
+	for (window = windows.head; window; window = window->next) {
+		if (writeString(file, idNames[window->id])) return -1;
+		for (size_t i = 0; i < BufferCap; ++i) {
+			time_t time = bufferTime(&window->buffer, i);
+			const char *line = bufferLine(&window->buffer, i);
+			if (!line) continue;
+			if (writeTime(file, time)) return -1;
+			if (writeString(file, line)) return -1;
+		}
+		if (writeTime(file, 0)) return -1;
+	}
+	return fclose(file);
+}
+
+static size_t readSize(FILE *file) {
+	size_t value;
+	fread(&value, sizeof(value), 1, file);
+	if (ferror(file)) err(EX_IOERR, "fread");
+	if (feof(file)) errx(EX_DATAERR, "unexpected eof");
+	return value;
+}
+static time_t readTime(FILE *file) {
+	time_t time;
+	fread(&time, sizeof(time), 1, file);
+	if (ferror(file)) err(EX_IOERR, "fread");
+	if (feof(file)) errx(EX_DATAERR, "unexpected eof");
+	return time;
+}
+static ssize_t readString(FILE *file, char **buf, size_t *cap) {
+	ssize_t len = getdelim(buf, cap, '\0', file);
+	if (len < 0 && !feof(file)) err(EX_IOERR, "getdelim");
+	return len;
+}
+
+void uiLoad(const char *name) {
+	FILE *file = dataOpen(name, "r");
+	if (!file) {
+		if (errno != ENOENT) exit(EX_NOINPUT);
+		file = dataOpen(name, "w");
+		if (!file) exit(EX_CANTCREAT);
+		fclose(file);
+		return;
+	}
+
+	size_t signature = readSize(file);
+	signatureVersion(signature);
+
+	char *buf = NULL;
+	size_t cap = 0;
+	while (0 < readString(file, &buf, &cap)) {
+		struct Window *window = windowFor(idFor(buf));
+		for (;;) {
+			time_t time = readTime(file);
+			if (!time) break;
+			readString(file, &buf, &cap);
+			bufferPush(&window->buffer, time, buf);
+		}
+		reflow(window);
+		// TODO: Place some marker of end of save.
+	}
+
+	free(buf);
+	fclose(file);
+}