Add -s to save and load buffers
parent
65603d5138
commit
b59431bb15
27
catgirl.1
27
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
|
||||
|
|
17
chat.c
17
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();
|
||||
|
|
4
chat.h
4
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,
|
||||
|
|
8
config.c
8
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);
|
||||
|
|
176
ui.c
176
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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue