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 n Ar nick
|
||||||
.Op Fl p Ar port
|
.Op Fl p Ar port
|
||||||
.Op Fl r Ar real
|
.Op Fl r Ar real
|
||||||
|
.Op Fl s Ar save
|
||||||
.Op Fl u Ar user
|
.Op Fl u Ar user
|
||||||
.Op Fl w Ar pass
|
.Op Fl w Ar pass
|
||||||
.Op Ar config ...
|
.Op Ar config ...
|
||||||
|
@ -123,6 +124,18 @@ Set realname to
|
||||||
.Ar real .
|
.Ar real .
|
||||||
The default realname is the same as the nickname.
|
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
|
.It Fl u Ar user , Cm user = Ar user
|
||||||
Set username to
|
Set username to
|
||||||
.Ar user .
|
.Ar user .
|
||||||
|
@ -324,7 +337,7 @@ The color numbers are as follows:
|
||||||
.Sh FILES
|
.Sh FILES
|
||||||
.Bl -tag -width Ds
|
.Bl -tag -width Ds
|
||||||
.It Pa $XDG_CONFIG_DIRS/catgirl
|
.It Pa $XDG_CONFIG_DIRS/catgirl
|
||||||
Configuration files are search for first in
|
Configuration files are searched for first in
|
||||||
.Ev $XDG_CONFIG_HOME ,
|
.Ev $XDG_CONFIG_HOME ,
|
||||||
usually
|
usually
|
||||||
.Pa ~/.config ,
|
.Pa ~/.config ,
|
||||||
|
@ -334,6 +347,18 @@ usually
|
||||||
.Pa /etc/xdg .
|
.Pa /etc/xdg .
|
||||||
.It Pa ~/.config/catgirl
|
.It Pa ~/.config/catgirl
|
||||||
The most likely location of configuration files.
|
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
|
.El
|
||||||
.
|
.
|
||||||
.Sh EXAMPLES
|
.Sh EXAMPLES
|
||||||
|
|
17
chat.c
17
chat.c
|
@ -47,6 +47,15 @@ size_t idNext = Network + 1;
|
||||||
|
|
||||||
struct Self self = { .color = Default };
|
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;
|
uint32_t hashInit;
|
||||||
|
|
||||||
int procPipe[2] = { -1, -1 };
|
int procPipe[2] = { -1, -1 };
|
||||||
|
@ -84,7 +93,7 @@ int main(int argc, char *argv[]) {
|
||||||
const char *user = NULL;
|
const char *user = NULL;
|
||||||
const char *real = 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[] = {
|
const struct option LongOpts[] = {
|
||||||
{ "insecure", no_argument, NULL, '!' },
|
{ "insecure", no_argument, NULL, '!' },
|
||||||
{ "copy", required_argument, NULL, 'C' },
|
{ "copy", required_argument, NULL, 'C' },
|
||||||
|
@ -99,6 +108,7 @@ int main(int argc, char *argv[]) {
|
||||||
{ "nick", required_argument, NULL, 'n' },
|
{ "nick", required_argument, NULL, 'n' },
|
||||||
{ "port", required_argument, NULL, 'p' },
|
{ "port", required_argument, NULL, 'p' },
|
||||||
{ "real", required_argument, NULL, 'r' },
|
{ "real", required_argument, NULL, 'r' },
|
||||||
|
{ "save", required_argument, NULL, 's' },
|
||||||
{ "user", required_argument, NULL, 'u' },
|
{ "user", required_argument, NULL, 'u' },
|
||||||
{ "debug", no_argument, NULL, 'v' },
|
{ "debug", no_argument, NULL, 'v' },
|
||||||
{ "pass", required_argument, NULL, 'w' },
|
{ "pass", required_argument, NULL, 'w' },
|
||||||
|
@ -121,6 +131,7 @@ int main(int argc, char *argv[]) {
|
||||||
break; case 'n': nick = optarg;
|
break; case 'n': nick = optarg;
|
||||||
break; case 'p': port = optarg;
|
break; case 'p': port = optarg;
|
||||||
break; case 'r': real = optarg;
|
break; case 'r': real = optarg;
|
||||||
|
break; case 's': save = optarg;
|
||||||
break; case 'u': user = optarg;
|
break; case 'u': user = optarg;
|
||||||
break; case 'v': self.debug = true;
|
break; case 'v': self.debug = true;
|
||||||
break; case 'w': pass = optarg;
|
break; case 'w': pass = optarg;
|
||||||
|
@ -154,6 +165,10 @@ int main(int argc, char *argv[]) {
|
||||||
if (privFile) fclose(privFile);
|
if (privFile) fclose(privFile);
|
||||||
|
|
||||||
uiInit();
|
uiInit();
|
||||||
|
if (save) {
|
||||||
|
uiLoad(save);
|
||||||
|
atexit(exitSave);
|
||||||
|
}
|
||||||
uiShowID(Network);
|
uiShowID(Network);
|
||||||
uiFormat(Network, Cold, NULL, "Traveling...");
|
uiFormat(Network, Cold, NULL, "Traveling...");
|
||||||
uiDraw();
|
uiDraw();
|
||||||
|
|
4
chat.h
4
chat.h
|
@ -26,6 +26,8 @@
|
||||||
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
||||||
#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
|
#define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
|
||||||
|
|
||||||
|
#define XDG_SUBDIR "catgirl"
|
||||||
|
|
||||||
typedef unsigned char byte;
|
typedef unsigned char byte;
|
||||||
|
|
||||||
int procPipe[2];
|
int procPipe[2];
|
||||||
|
@ -144,6 +146,8 @@ void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str);
|
||||||
void uiFormat(
|
void uiFormat(
|
||||||
size_t id, enum Heat heat, const time_t *time, const char *format, ...
|
size_t id, enum Heat heat, const time_t *time, const char *format, ...
|
||||||
) __attribute__((format(printf, 4, 5)));
|
) __attribute__((format(printf, 4, 5)));
|
||||||
|
void uiLoad(const char *name);
|
||||||
|
int uiSave(const char *name);
|
||||||
|
|
||||||
enum Edit {
|
enum Edit {
|
||||||
EditHead,
|
EditHead,
|
||||||
|
|
8
config.c
8
config.c
|
@ -24,8 +24,6 @@
|
||||||
|
|
||||||
#include "chat.h"
|
#include "chat.h"
|
||||||
|
|
||||||
#define CONFIG_DIR "catgirl"
|
|
||||||
|
|
||||||
FILE *configOpen(const char *path, const char *mode) {
|
FILE *configOpen(const char *path, const char *mode) {
|
||||||
if (path[0] == '/' || path[0] == '.') goto local;
|
if (path[0] == '/' || path[0] == '.') goto local;
|
||||||
|
|
||||||
|
@ -35,10 +33,10 @@ FILE *configOpen(const char *path, const char *mode) {
|
||||||
|
|
||||||
char buf[PATH_MAX];
|
char buf[PATH_MAX];
|
||||||
if (configHome) {
|
if (configHome) {
|
||||||
snprintf(buf, sizeof(buf), "%s/" CONFIG_DIR "/%s", configHome, path);
|
snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path);
|
||||||
} else {
|
} else {
|
||||||
if (!home) goto local;
|
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);
|
FILE *file = fopen(buf, mode);
|
||||||
if (file) return file;
|
if (file) return file;
|
||||||
|
@ -48,7 +46,7 @@ FILE *configOpen(const char *path, const char *mode) {
|
||||||
while (*configDirs) {
|
while (*configDirs) {
|
||||||
size_t len = strcspn(configDirs, ":");
|
size_t len = strcspn(configDirs, ":");
|
||||||
snprintf(
|
snprintf(
|
||||||
buf, sizeof(buf), "%.*s/" CONFIG_DIR "/%s",
|
buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s",
|
||||||
(int)len, configDirs, path
|
(int)len, configDirs, path
|
||||||
);
|
);
|
||||||
file = fopen(buf, mode);
|
file = fopen(buf, mode);
|
||||||
|
|
176
ui.c
176
ui.c
|
@ -20,11 +20,14 @@
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <curses.h>
|
#include <curses.h>
|
||||||
#include <err.h>
|
#include <err.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <limits.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sysexits.h>
|
#include <sysexits.h>
|
||||||
#include <term.h>
|
#include <term.h>
|
||||||
#include <termios.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");
|
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 };
|
enum { WindowLines = BufferCap };
|
||||||
struct Window {
|
struct Window {
|
||||||
size_t id;
|
size_t id;
|
||||||
|
@ -532,9 +542,8 @@ static void reflow(struct Window *window) {
|
||||||
werase(window->pad);
|
werase(window->pad);
|
||||||
wmove(window->pad, WindowLines - 1, 0);
|
wmove(window->pad, WindowLines - 1, 0);
|
||||||
window->unreadLines = 0;
|
window->unreadLines = 0;
|
||||||
struct Buffer *buffer = &window->buffer;
|
|
||||||
for (size_t i = 0; i < BufferCap; ++i) {
|
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;
|
if (!line) continue;
|
||||||
waddch(window->pad, '\n');
|
waddch(window->pad, '\n');
|
||||||
if (i >= (size_t)(BufferCap - window->unreadCount)) {
|
if (i >= (size_t)(BufferCap - window->unreadCount)) {
|
||||||
|
@ -557,12 +566,12 @@ static void resize(void) {
|
||||||
statusUpdate();
|
statusUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void bufferList(struct Buffer *buffer) {
|
static void bufferList(const struct Buffer *buffer) {
|
||||||
uiHide();
|
uiHide();
|
||||||
waiting = true;
|
waiting = true;
|
||||||
for (size_t i = 0; i < BufferCap; ++i) {
|
for (size_t i = 0; i < BufferCap; ++i) {
|
||||||
time_t time = buffer->times[(buffer->len + i) % BufferCap];
|
time_t time = bufferTime(buffer, i);
|
||||||
const char *line = buffer->lines[(buffer->len + i) % BufferCap];
|
const char *line = bufferLine(buffer, i);
|
||||||
if (!line) continue;
|
if (!line) continue;
|
||||||
|
|
||||||
struct tm *tm = localtime(&time);
|
struct tm *tm = localtime(&time);
|
||||||
|
@ -848,3 +857,160 @@ void uiRead(void) {
|
||||||
}
|
}
|
||||||
inputUpdate();
|
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