From 843160236381d0c76bef1eac89e556920d700a9d Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sat, 1 Feb 2020 01:18:01 -0500
Subject: Blindly implement login flow

---
 Makefile | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)
 create mode 100644 Makefile

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..30bcf66
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,18 @@
+LIBRESSL_PREFIX = /usr/local
+CFLAGS += -I${LIBRESSL_PREFIX}/include
+LDFLAGS += -L${LIBRESSL_PREFIX}/lib
+
+CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
+LDLIBS = -lcrypto -ltls
+
+OBJS += chat.o
+OBJS += handle.o
+OBJS += irc.o
+
+catgirl: ${OBJS}
+	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
+
+${OBJS}: chat.h
+
+clean:
+	rm -f catgirl ${OBJS}
-- 
cgit 1.4.1-2-gfad0


From e289ff6b18e643eea4c72e04f7c0dc6ff768a335 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sat, 1 Feb 2020 02:55:07 -0500
Subject: Add term stuff

Copied almost verbatim from existing catgirl... I think I did a better
job on that state machine this time tbh.
---
 Makefile |  1 +
 chat.h   | 17 ++++++++++++++
 term.c   | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 97 insertions(+)
 create mode 100644 term.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 30bcf66..999b491 100644
--- a/Makefile
+++ b/Makefile
@@ -8,6 +8,7 @@ LDLIBS = -lcrypto -ltls
 OBJS += chat.o
 OBJS += handle.o
 OBJS += irc.o
+OBJS += term.o
 
 catgirl: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
diff --git a/chat.h b/chat.h
index 8c13d49..93014ef 100644
--- a/chat.h
+++ b/chat.h
@@ -91,6 +91,23 @@ void ircFormat(const char *format, ...)
 
 void handle(struct Message msg);
 
+enum TermMode {
+	TermFocus,
+	TermPaste,
+};
+enum TermEvent {
+	TermNone,
+	TermFocusIn,
+	TermFocusOut,
+	TermPasteStart,
+	TermPasteEnd,
+};
+void termInit(void);
+void termNoFlow(void);
+void termTitle(const char *title);
+void termMode(enum TermMode mode, bool set);
+enum TermEvent termEvent(char ch);
+
 #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4)
 
 static const char Base64[64] = {
diff --git a/term.c b/term.c
new file mode 100644
index 0000000..ade5392
--- /dev/null
+++ b/term.c
@@ -0,0 +1,79 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "chat.h"
+
+static bool xterm;
+
+void termInit(void) {
+	const char *term = getenv("TERM");
+	xterm = (term && !strncmp(term, "xterm", 5));
+}
+
+void termNoFlow(void) {
+	struct termios attr;
+	int error = tcgetattr(STDIN_FILENO, &attr);
+	if (error) return;
+	attr.c_iflag &= ~IXON;
+	attr.c_cc[VDISCARD] = _POSIX_VDISABLE;
+	tcsetattr(STDIN_FILENO, TCSANOW, &attr);
+}
+
+void termTitle(const char *title) {
+	if (!xterm) return;
+	printf("\33]0;%s\33\\", title);
+	fflush(stdout);
+}
+
+static void privateMode(const char *mode, bool set) {
+	printf("\33[?%s%c", mode, (set ? 'h' : 'l'));
+	fflush(stdout);
+}
+
+void termMode(enum TermMode mode, bool set) {
+	switch (mode) {
+		break; case TermFocus: privateMode("1004", set);
+		break; case TermPaste: privateMode("2004", set);
+	}
+}
+
+enum { Esc = '\33' };
+
+enum TermEvent termEvent(char ch) {
+	static int st;
+#define T(st, ch) ((st) << 8 | (ch))
+	switch (T(st, ch)) {
+		break; case T(0, Esc): st = 1;
+		break; case T(1, '['): st = 2;
+		break; case T(2, 'I'): st = 0; return TermFocusIn;
+		break; case T(2, 'O'): st = 0; return TermFocusOut;
+		break; case T(2, '2'): st = 3;
+		break; case T(3, '0'): st = 4;
+		break; case T(4, '0'): st = 5;
+		break; case T(5, '~'): st = 0; return TermPasteStart;
+		break; case T(4, '1'): st = 6;
+		break; case T(6, '~'): st = 0; return TermPasteEnd;
+		break; default: st = 0;
+	}
+	return 0;
+#undef T
+}
-- 
cgit 1.4.1-2-gfad0


From e5363bcae0f726455fb4198cd21d46721ad5e39a Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sat, 1 Feb 2020 19:37:48 -0500
Subject: Implement the beginnings of UI

It takes so much code to do anything in curses...
---
 Makefile |   3 +-
 chat.c   |   8 +++
 chat.h   |  22 +++++++-
 handle.c |  21 ++++++++
 irc.c    |   6 +--
 ui.c     | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 228 insertions(+), 6 deletions(-)
 create mode 100644 ui.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 999b491..4af59ee 100644
--- a/Makefile
+++ b/Makefile
@@ -3,12 +3,13 @@ CFLAGS += -I${LIBRESSL_PREFIX}/include
 LDFLAGS += -L${LIBRESSL_PREFIX}/lib
 
 CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
-LDLIBS = -lcrypto -ltls
+LDLIBS = -lcurses -lcrypto -ltls
 
 OBJS += chat.o
 OBJS += handle.o
 OBJS += irc.o
 OBJS += term.o
+OBJS += ui.o
 
 catgirl: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
diff --git a/chat.c b/chat.c
index 462faa0..47227d5 100644
--- a/chat.c
+++ b/chat.c
@@ -15,6 +15,7 @@
  */
 
 #include <err.h>
+#include <locale.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -33,6 +34,8 @@ size_t idNext = Network + 1;
 struct Self self;
 
 int main(int argc, char *argv[]) {
+	setlocale(LC_CTYPE, "");
+
 	bool insecure = false;
 	const char *host = NULL;
 	const char *port = "6697";
@@ -71,6 +74,10 @@ int main(int argc, char *argv[]) {
 	if (!real) real = nick;
 
 	ircConfig(insecure, cert, priv);
+
+	uiInit();
+	uiFormat(Network, Cold, NULL, "Traveling...");
+	uiDraw();
 	
 	int irc = ircConnect(host, port);
 	if (pass) ircFormat("PASS :%s\r\n", pass);
@@ -80,6 +87,7 @@ int main(int argc, char *argv[]) {
 	ircFormat("USER %s 0 * :%s\r\n", user, real);
 
 	for (;;) {
+		uiDraw();
 		ircRecv();
 	}
 }
diff --git a/chat.h b/chat.h
index 93014ef..be3952c 100644
--- a/chat.h
+++ b/chat.h
@@ -82,6 +82,18 @@ struct Message {
 	char *params[ParamCap];
 };
 
+#define B "\2"
+#define C "\3"
+#define R "\17"
+#define V "\26"
+#define I "\35"
+#define U "\37"
+enum Color {
+	White, Black, Blue, Green, Red, Brown, Magenta, Orange,
+	Yellow, LightGreen, Cyan, LightCyan, LightBlue, Pink, Gray, LightGray,
+	Default = 99,
+};
+
 void ircConfig(bool insecure, const char *cert, const char *priv);
 int ircConnect(const char *host, const char *port);
 void ircRecv(void);
@@ -91,6 +103,14 @@ void ircFormat(const char *format, ...)
 
 void handle(struct Message msg);
 
+enum Heat { Cold, Warm, Hot };
+void uiInit(void);
+void uiDraw(void);
+void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str);
+void uiFormat(
+	size_t id, enum Heat heat, const struct tm *time, const char *format, ...
+) __attribute__((format(printf, 4, 5)));
+
 enum TermMode {
 	TermFocus,
 	TermPaste,
@@ -109,11 +129,9 @@ void termMode(enum TermMode mode, bool set);
 enum TermEvent termEvent(char ch);
 
 #define BASE64_SIZE(len) (1 + ((len) + 2) / 3 * 4)
-
 static const char Base64[64] = {
 	"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
 };
-
 static inline void base64(char *dst, const byte *src, size_t len) {
 	size_t i = 0;
 	while (len > 2) {
diff --git a/handle.c b/handle.c
index 96bd2a2..b2b2b8d 100644
--- a/handle.c
+++ b/handle.c
@@ -136,11 +136,32 @@ static void handleReplyWelcome(struct Message *msg) {
 	if (self.join) ircFormat("JOIN :%s\r\n", self.join);
 }
 
+static void handleReplyISupport(struct Message *msg) {
+	// TODO: Extract CHANTYPES and PREFIX for future use.
+	for (size_t i = 1; i < ParamCap; ++i) {
+		if (!msg->params[i]) break;
+		char *key = strsep(&msg->params[i], "=");
+		if (!msg->params[i]) continue;
+		if (!strcmp(key, "NETWORK")) {
+			uiFormat(Network, Cold, NULL, "You arrive in %s", msg->params[i]);
+		}
+	}
+}
+
+static void handleReplyMOTD(struct Message *msg) {
+	require(msg, false, 2);
+	char *line = msg->params[1];
+	if (!strncmp(line, "- ", 2)) line += 2;
+	uiFormat(Network, Cold, NULL, "%s", line);
+}
+
 static const struct Handler {
 	const char *cmd;
 	Handler *fn;
 } Handlers[] = {
 	{ "001", handleReplyWelcome },
+	{ "005", handleReplyISupport },
+	{ "372", handleReplyMOTD },
 	{ "900", handleReplyLoggedIn },
 	{ "904", handleErrorSASLFail },
 	{ "905", handleErrorSASLFail },
diff --git a/irc.c b/irc.c
index cf8aab7..f07fcc8 100644
--- a/irc.c
+++ b/irc.c
@@ -104,10 +104,10 @@ int ircConnect(const char *host, const char *port) {
 static void debug(char dir, const char *line) {
 	if (!self.debug) return;
 	size_t len = strcspn(line, "\r\n");
-	/*uiFormat(
-		Debug, Cold, NULL, "\3%02d%c%c\3 %.*s",
+	uiFormat(
+		Debug, Cold, NULL, C"%d%c%c"C" %.*s",
 		Gray, dir, dir, (int)len, line
-	);*/
+	);
 	if (!isatty(STDERR_FILENO)) {
 		fprintf(stderr, "%c%c %.*s\n", dir, dir, (int)len, line);
 	}
diff --git a/ui.c b/ui.c
new file mode 100644
index 0000000..0295c8d
--- /dev/null
+++ b/ui.c
@@ -0,0 +1,174 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <curses.h>
+#include <err.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+#include <time.h>
+
+#include "chat.h"
+
+#ifndef A_ITALIC
+#define A_ITALIC A_UNDERLINE
+#endif
+
+#define BOTTOM (LINES - 1)
+#define RIGHT (COLS - 1)
+#define WINDOW_LINES (LINES - 2)
+
+static short colorPairs;
+
+static void colorInit(void) {
+	start_color();
+	use_default_colors();
+	for (short pair = 0; pair < 16; ++pair) {
+		init_pair(1 + pair, pair % COLORS, -1);
+	}
+	colorPairs = 17;
+}
+
+static attr_t colorAttr(short fg) {
+	return (fg >= COLORS ? A_BOLD : A_NORMAL);
+}
+
+static short colorPair(short fg, short bg) {
+	if (bg == -1) return 1 + fg;
+	for (short pair = 17; pair < colorPairs; ++pair) {
+		short f, b;
+		pair_content(pair, &f, &b);
+		if (f == fg && b == bg) return pair;
+	}
+	init_pair(colorPairs, fg, bg);
+	return colorPairs++;
+}
+
+enum {
+	InputCols = 512,
+	PadLines = 512,
+};
+
+static WINDOW *status;
+static WINDOW *input;
+
+struct Window {
+	size_t id;
+	WINDOW *pad;
+	enum Heat heat;
+	int unread;
+	int scroll;
+	bool mark;
+	struct Window *prev;
+	struct Window *next;
+};
+
+static struct {
+	struct Window *active;
+	struct Window *other;
+	struct Window *head;
+	struct Window *tail;
+} windows;
+
+static void windowAdd(struct Window *window) {
+	if (windows.tail) windows.tail->next = window;
+	window->prev = windows.tail;
+	window->next = NULL;
+	windows.tail = window;
+	if (!windows.head) windows.head = window;
+}
+
+static void windowRemove(struct Window *window) {
+	if (window->prev) window->prev->next = window->next;
+	if (window->next) window->next->prev = window->prev;
+	if (windows.head == window) windows.head = window->next;
+	if (windows.tail == window) windows.tail = window->prev;
+}
+
+static struct Window *windowFor(size_t id) {
+	struct Window *window;
+	for (window = windows.head; window; window = window->next) {
+		if (window->id == id) return window;
+	}
+	window = malloc(sizeof(*window));
+	if (!window) err(EX_OSERR, "malloc");
+	window->id = id;
+	window->pad = newpad(PadLines, COLS);
+	wsetscrreg(window->pad, 0, PadLines - 1);
+	scrollok(window->pad, true);
+	wmove(window->pad, PadLines - 1, 0);
+	window->heat = Cold;
+	window->unread = 0;
+	window->scroll = PadLines;
+	window->mark = true;
+	windowAdd(window);
+	return window;
+}
+
+void uiInit(void) {
+	initscr();
+	cbreak();
+	noecho();
+	termInit();
+	termNoFlow();
+	def_prog_mode();
+	colorInit();
+	status = newwin(1, COLS, 0, 0);
+	input = newpad(1, InputCols);
+	keypad(input, true);
+	nodelay(input, true);
+	windows.active = windowFor(Network);
+}
+
+void uiDraw(void) {
+	wnoutrefresh(status);
+	pnoutrefresh(
+		windows.active->pad,
+		windows.active->scroll - WINDOW_LINES, 0,
+		1, 0,
+		BOTTOM - 1, RIGHT
+	);
+	// TODO: Input scrolling.
+	pnoutrefresh(
+		input,
+		0, 0,
+		BOTTOM, 0,
+		BOTTOM, RIGHT
+	);
+	doupdate();
+}
+
+void uiWrite(size_t id, enum Heat heat, const struct tm *time, const char *str) {
+	(void)time;
+	struct Window *window = windowFor(id);
+	waddch(window->pad, '\n');
+	waddstr(window->pad, str);
+}
+
+void uiFormat(
+	size_t id, enum Heat heat, const struct tm *time, const char *format, ...
+) {
+	char buf[1024];
+	va_list ap;
+	va_start(ap, format);
+	int len = vsnprintf(buf, sizeof(buf), format, ap);
+	va_end(ap);
+	assert((size_t)len < sizeof(buf));
+	uiWrite(id, heat, time, buf);
+}
-- 
cgit 1.4.1-2-gfad0


From d59666cb25de1e0a9322e22d1fa94d26583383b2 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sat, 1 Feb 2020 21:55:05 -0500
Subject: Generate tags file

---
 .gitignore | 1 +
 Makefile   | 9 ++++++++-
 2 files changed, 9 insertions(+), 1 deletion(-)

(limited to 'Makefile')

diff --git a/.gitignore b/.gitignore
index ab80afd..64b2b13 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 *.o
 catgirl
+tags
diff --git a/Makefile b/Makefile
index 4af59ee..ce27d4e 100644
--- a/Makefile
+++ b/Makefile
@@ -11,10 +11,17 @@ OBJS += irc.o
 OBJS += term.o
 OBJS += ui.o
 
+dev: tags all
+
+all: catgirl
+
 catgirl: ${OBJS}
 	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
 
 ${OBJS}: chat.h
 
+tags: *.h *.c
+	ctags -w *.h *.c
+
 clean:
-	rm -f catgirl ${OBJS}
+	rm -f tags catgirl ${OBJS}
-- 
cgit 1.4.1-2-gfad0


From 5c328c7a8801d6a4aded769092ead9715d4ecf98 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sun, 2 Feb 2020 19:34:35 -0500
Subject: Remove term.c in favor of more curses APIs

---
 Makefile |  1 -
 chat.h   | 15 ---------------
 term.c   | 66 ----------------------------------------------------------------
 ui.c     | 28 +++++++++++++++++++++++++--
 4 files changed, 26 insertions(+), 84 deletions(-)
 delete mode 100644 term.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index ce27d4e..6ba0ba5 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,6 @@ LDLIBS = -lcurses -lcrypto -ltls
 OBJS += chat.o
 OBJS += handle.o
 OBJS += irc.o
-OBJS += term.o
 OBJS += ui.o
 
 dev: tags all
diff --git a/chat.h b/chat.h
index 8a806f1..43f62fd 100644
--- a/chat.h
+++ b/chat.h
@@ -118,21 +118,6 @@ void uiFormat(
 	size_t id, enum Heat heat, const struct tm *time, const char *format, ...
 ) __attribute__((format(printf, 4, 5)));
 
-enum TermMode {
-	TermFocus,
-	TermPaste,
-};
-enum TermEvent {
-	TermNone,
-	TermFocusIn,
-	TermFocusOut,
-	TermPasteStart,
-	TermPasteEnd,
-};
-void termNoFlow(void);
-void termMode(enum TermMode mode, bool set);
-enum TermEvent termEvent(char ch);
-
 static inline enum Color hash(const char *str) {
 	if (*str == '~') str++;
 	uint32_t hash = 0;
diff --git a/term.c b/term.c
deleted file mode 100644
index 427cac6..0000000
--- a/term.c
+++ /dev/null
@@ -1,66 +0,0 @@
-/* Copyright (C) 2018, 2020  C. McEnroe <june@causal.agency>
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-#include <stdbool.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <termios.h>
-#include <unistd.h>
-
-#include "chat.h"
-
-void termNoFlow(void) {
-	struct termios attr;
-	int error = tcgetattr(STDIN_FILENO, &attr);
-	if (error) return;
-	attr.c_iflag &= ~IXON;
-	attr.c_cc[VDISCARD] = _POSIX_VDISABLE;
-	tcsetattr(STDIN_FILENO, TCSANOW, &attr);
-}
-
-static void privateMode(const char *mode, bool set) {
-	printf("\33[?%s%c", mode, (set ? 'h' : 'l'));
-	fflush(stdout);
-}
-
-void termMode(enum TermMode mode, bool set) {
-	switch (mode) {
-		break; case TermFocus: privateMode("1004", set);
-		break; case TermPaste: privateMode("2004", set);
-	}
-}
-
-enum { Esc = '\33' };
-
-enum TermEvent termEvent(char ch) {
-	static int st;
-#define T(st, ch) ((st) << 8 | (ch))
-	switch (T(st, ch)) {
-		break; case T(0, Esc): st = 1;
-		break; case T(1, '['): st = 2;
-		break; case T(2, 'I'): st = 0; return TermFocusIn;
-		break; case T(2, 'O'): st = 0; return TermFocusOut;
-		break; case T(2, '2'): st = 3;
-		break; case T(3, '0'): st = 4;
-		break; case T(4, '0'): st = 5;
-		break; case T(5, '~'): st = 0; return TermPasteStart;
-		break; case T(4, '1'): st = 6;
-		break; case T(6, '~'): st = 0; return TermPasteEnd;
-		break; default: st = 0;
-	}
-	return 0;
-#undef T
-}
diff --git a/ui.c b/ui.c
index f434289..5d626ce 100644
--- a/ui.c
+++ b/ui.c
@@ -25,7 +25,9 @@
 #include <string.h>
 #include <sysexits.h>
 #include <term.h>
+#include <termios.h>
 #include <time.h>
+#include <unistd.h>
 #include <wchar.h>
 #include <wctype.h>
 
@@ -125,6 +127,23 @@ static struct Window *windowFor(size_t id) {
 	return window;
 }
 
+enum {
+	KeyFocusIn = KEY_MAX + 1,
+	KeyFocusOut,
+	KeyPasteOn,
+	KeyPasteOff,
+};
+
+static void disableFlowControl(void) {
+	struct termios term;
+	int error = tcgetattr(STDOUT_FILENO, &term);
+	if (error) err(EX_OSERR, "tcgetattr");
+	term.c_iflag &= ~IXON;
+	term.c_cc[VDISCARD] = _POSIX_VDISABLE;
+	error = tcsetattr(STDOUT_FILENO, TCSADRAIN, &term);
+	if (error) err(EX_OSERR, "tcsetattr");
+}
+
 static void errExit(int eval) {
 	(void)eval;
 	reset_shell_mode();
@@ -134,15 +153,20 @@ void uiInit(void) {
 	initscr();
 	cbreak();
 	noecho();
-	termNoFlow();
+	disableFlowControl();
 	def_prog_mode();
 	err_set_exit(errExit);
-	colorInit();
+
 	if (!to_status_line && !strncmp(termname(), "xterm", 5)) {
 		to_status_line = "\33]2;";
 		from_status_line = "\7";
 	}
+	define_key("\33[I", KeyFocusIn);
+	define_key("\33[O", KeyFocusOut);
+	define_key("\33[200~", KeyPasteOn);
+	define_key("\33[201~", KeyPasteOff);
 
+	colorInit();
 	status = newwin(1, COLS, 0, 0);
 	input = newpad(1, InputCols);
 	keypad(input, true);
-- 
cgit 1.4.1-2-gfad0


From 9944dc484bf8cc7d5f1c506610a0593202bb5f92 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Tue, 4 Feb 2020 20:23:55 -0500
Subject: Split showing style codes and word wrapping

---
 Makefile |   1 +
 chat.c   |   2 +-
 chat.h   |   4 ++
 edit.c   |  30 ++++++++++++
 ui.c     | 168 +++++++++++++++++++++++++++++++++++++++++----------------------
 5 files changed, 146 insertions(+), 59 deletions(-)
 create mode 100644 edit.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 6ba0ba5..63f719d 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,7 @@ CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
 LDLIBS = -lcurses -lcrypto -ltls
 
 OBJS += chat.o
+OBJS += edit.o
 OBJS += handle.o
 OBJS += irc.o
 OBJS += ui.o
diff --git a/chat.c b/chat.c
index 1656a53..545eca6 100644
--- a/chat.c
+++ b/chat.c
@@ -41,7 +41,7 @@ enum Color idColors[IDCap] = {
 
 size_t idNext = Network + 1;
 
-struct Self self;
+struct Self self = { .color = Default };
 
 static volatile sig_atomic_t signals[NSIG];
 static void signalHandler(int signal) {
diff --git a/chat.h b/chat.h
index 8de5df1..c754357 100644
--- a/chat.h
+++ b/chat.h
@@ -72,6 +72,7 @@ extern struct Self {
 	char *chanTypes;
 	char *prefixes;
 	char *nick;
+	enum Color color;
 } self;
 
 static inline void set(char **field, const char *value) {
@@ -121,6 +122,9 @@ void uiFormat(
 	size_t id, enum Heat heat, const time_t *time, const char *format, ...
 ) __attribute__((format(printf, 4, 5)));
 
+const char *editHead(void);
+const char *editTail(void);
+
 static inline enum Color hash(const char *str) {
 	if (*str == '~') str++;
 	uint32_t hash = 0;
diff --git a/edit.c b/edit.c
new file mode 100644
index 0000000..446e0e9
--- /dev/null
+++ b/edit.c
@@ -0,0 +1,30 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "chat.h"
+
+const char *editHead(void) {
+	return "foo\0033bar";
+}
+
+const char *editTail(void) {
+	return "baz\3";
+}
diff --git a/ui.c b/ui.c
index e93c08c..568df8d 100644
--- a/ui.c
+++ b/ui.c
@@ -268,71 +268,18 @@ static void styleParse(struct Style *style, const char **str, size_t *len) {
 	*len = strcspn(*str, "\2\3\17\26\35\37");
 }
 
-static int wordWidth(const char *str) {
-	size_t len = strcspn(str, " ");
-	int width = 0;
-	while (len) {
-		wchar_t wc;
-		int n = mbtowc(&wc, str, len);
-		if (n < 1) return width + len;
-		width += (iswprint(wc) ? wcwidth(wc) : 0);
-		str += n;
-		len -= n;
-	}
-	return width;
-}
-
-static void styleAdd(WINDOW *win, const char *str, bool show) {
-	int y, x, width;
-	getmaxyx(win, y, width);
-
+static void statusAdd(const char *str) {
 	size_t len;
-	int align = 0;
 	struct Style style = Reset;
 	while (*str) {
-		if (*str == '\t') {
-			waddch(win, ' ');
-			getyx(win, y, align);
-			str++;
-		} else if (*str == ' ') {
-			getyx(win, y, x);
-			const char *word = &str[strspn(str, " ")];
-			if (width - x - 1 <= wordWidth(word)) {
-				waddch(win, '\n');
-				getyx(win, y, x);
-				wmove(win, y, align);
-				str = word;
-			} else {
-				waddch(win, ' ');
-				str++;
-			}
-		}
-
-		const char *code = str;
 		styleParse(&style, &str, &len);
-		if (show) {
-			wattr_set(win, A_BOLD | A_REVERSE, 0, NULL);
-			switch (*code) {
-				break; case '\2':  waddch(win, 'B');
-				break; case '\3':  waddch(win, 'C');
-				break; case '\17': waddch(win, 'O');
-				break; case '\26': waddch(win, 'R');
-				break; case '\35': waddch(win, 'I');
-				break; case '\37': waddch(win, 'U');
-			}
-			if (str - code > 1) waddnstr(win, &code[1], str - &code[1]);
-		}
-
-		size_t ws = strcspn(str, "\t ");
-		if (ws < len) len = ws;
-
 		wattr_set(
-			win,
+			status,
 			style.attr | colorAttr(mapColor(style.fg)),
 			colorPair(mapColor(style.fg), mapColor(style.bg)),
 			NULL
 		);
-		waddnstr(win, str, len);
+		waddnstr(status, str, len);
 		str += len;
 	}
 }
@@ -354,7 +301,7 @@ static void statusUpdate(void) {
 			idColors[window->id]
 		);
 		if (!window->unread) buf[unread] = '\0';
-		styleAdd(status, buf, false);
+		statusAdd(buf);
 	}
 	wclrtoeol(status);
 
@@ -399,6 +346,61 @@ void uiShowNum(size_t num) {
 	windowShow(window);
 }
 
+static int wordWidth(const char *str) {
+	size_t len = strcspn(str, " ");
+	int width = 0;
+	while (len) {
+		wchar_t wc;
+		int n = mbtowc(&wc, str, len);
+		if (n < 1) return width + len;
+		width += (iswprint(wc) ? wcwidth(wc) : 0);
+		str += n;
+		len -= n;
+	}
+	return width;
+}
+
+static void wordWrap(WINDOW *win, const char *str) {
+	int y, x, width;
+	getmaxyx(win, y, width);
+
+	size_t len;
+	int align = 0;
+	struct Style style = Reset;
+	while (*str) {
+		if (*str == '\t') {
+			waddch(win, ' ');
+			getyx(win, y, align);
+			str++;
+		} else if (*str == ' ') {
+			getyx(win, y, x);
+			const char *word = &str[strspn(str, " ")];
+			if (width - x - 1 <= wordWidth(word)) {
+				waddch(win, '\n');
+				getyx(win, y, x);
+				wmove(win, y, align);
+				str = word;
+			} else {
+				waddch(win, ' ');
+				str++;
+			}
+		}
+
+		styleParse(&style, &str, &len);
+		size_t ws = strcspn(str, "\t ");
+		if (ws < len) len = ws;
+
+		wattr_set(
+			win,
+			style.attr | colorAttr(mapColor(style.fg)),
+			colorPair(mapColor(style.fg), mapColor(style.bg)),
+			NULL
+		);
+		waddnstr(win, str, len);
+		str += len;
+	}
+}
+
 void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) {
 	(void)time;
 	struct Window *window = windowFor(id);
@@ -410,7 +412,7 @@ void uiWrite(size_t id, enum Heat heat, const time_t *time, const char *str) {
 		window->heat = heat;
 		statusUpdate();
 	}
-	styleAdd(window->pad, str, false);
+	wordWrap(window->pad, str);
 }
 
 void uiFormat(
@@ -425,6 +427,55 @@ void uiFormat(
 	uiWrite(id, heat, time, buf);
 }
 
+static void inputAdd(struct Style *style, const char *str) {
+	size_t len;
+	while (*str) {
+		const char *code = str;
+		styleParse(style, &str, &len);
+		wattr_set(input, A_BOLD | A_REVERSE, 0, NULL);
+		switch (*code) {
+			break; case '\2':  waddch(input, 'B');
+			break; case '\3':  waddch(input, 'C');
+			break; case '\17': waddch(input, 'O');
+			break; case '\26': waddch(input, 'R');
+			break; case '\35': waddch(input, 'I');
+			break; case '\37': waddch(input, 'U');
+		}
+		if (str - code > 1) waddnstr(input, &code[1], str - &code[1]);
+		wattr_set(
+			input,
+			style->attr | colorAttr(mapColor(style->fg)),
+			colorPair(mapColor(style->fg), mapColor(style->bg)),
+			NULL
+		);
+		waddnstr(input, str, len);
+		str += len;
+	}
+}
+
+static void inputUpdate(void) {
+	wmove(input, 0, 0);
+	wattr_set(
+		input,
+		colorAttr(mapColor(self.color)),
+		colorPair(mapColor(self.color), -1),
+		NULL
+	);
+	if (self.nick) {
+		waddch(input, '<');
+		waddstr(input, self.nick);
+		waddstr(input, "> ");
+	}
+
+	int y, x;
+	struct Style style = Reset;
+	inputAdd(&style, editHead());
+	getyx(input, y, x);
+	inputAdd(&style, editTail());
+	wclrtoeol(input);
+	wmove(input, y, x);
+}
+
 static void keyCode(int code) {
 	switch (code) {
 		break; case KEY_RESIZE:; // TODO
@@ -467,4 +518,5 @@ void uiRead(void) {
 		}
 		meta = false;
 	}
+	inputUpdate();
 }
-- 
cgit 1.4.1-2-gfad0


From 4cce893eab7403821ff211f64a7df05051fd6f52 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Wed, 5 Feb 2020 00:20:39 -0500
Subject: Add extremely basic editing and message sending

---
 Makefile  |  1 +
 chat.h    | 12 ++++++++++--
 command.c | 32 ++++++++++++++++++++++++++++++++
 edit.c    | 32 +++++++++++++++++++++++++-------
 ui.c      |  5 ++++-
 5 files changed, 72 insertions(+), 10 deletions(-)
 create mode 100644 command.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 63f719d..05f8bb8 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,7 @@ CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
 LDLIBS = -lcurses -lcrypto -ltls
 
 OBJS += chat.o
+OBJS += command.o
 OBJS += edit.o
 OBJS += handle.o
 OBJS += irc.o
diff --git a/chat.h b/chat.h
index c754357..c8b31c2 100644
--- a/chat.h
+++ b/chat.h
@@ -20,6 +20,7 @@
 #include <string.h>
 #include <sysexits.h>
 #include <time.h>
+#include <wchar.h>
 
 #define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
 #define BIT(x) x##Bit, x = 1 << x##Bit, x##Bit_ = x##Bit
@@ -109,6 +110,7 @@ void ircFormat(const char *format, ...)
 	__attribute__((format(printf, 1, 2)));
 
 void handle(struct Message msg);
+void command(size_t id, char *input);
 
 enum Heat { Cold, Warm, Hot };
 void uiInit(void);
@@ -122,8 +124,14 @@ void uiFormat(
 	size_t id, enum Heat heat, const time_t *time, const char *format, ...
 ) __attribute__((format(printf, 4, 5)));
 
-const char *editHead(void);
-const char *editTail(void);
+enum Edit {
+	EditKill,
+	EditInsert,
+	EditEnter,
+};
+void edit(size_t id, enum Edit op, wchar_t ch);
+char *editHead(void);
+char *editTail(void);
 
 static inline enum Color hash(const char *str) {
 	if (*str == '~') str++;
diff --git a/command.c b/command.c
new file mode 100644
index 0000000..ab05587
--- /dev/null
+++ b/command.c
@@ -0,0 +1,32 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "chat.h"
+
+void command(size_t id, char *input) {
+	ircFormat("PRIVMSG %s :%s\r\n", idNames[id], input);
+	struct Message msg = {
+		.nick = self.nick,
+		// TODO: .user,
+		.cmd = "PRIVMSG",
+		.params[0] = idNames[id],
+		.params[1] = input,
+	};
+	handle(msg);
+}
diff --git a/edit.c b/edit.c
index e142507..68593d1 100644
--- a/edit.c
+++ b/edit.c
@@ -23,22 +23,40 @@
 #include "chat.h"
 
 enum { Cap = 512 };
-static wchar_t buf[Cap] = L"foo\0033bar\3baz";
-static size_t len = 12;
-static size_t pos = 6;
+static wchar_t buf[Cap];
+static size_t len;
+static size_t pos;
 
-const char *editHead(void) {
+char *editHead(void) {
 	static char mbs[MB_LEN_MAX * Cap];
 	const wchar_t *ptr = buf;
-	size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs), NULL);
+	size_t n = wcsnrtombs(mbs, &ptr, pos, sizeof(mbs) - 1, NULL);
 	assert(n != (size_t)-1);
+	mbs[n] = '\0';
 	return mbs;
 }
 
-const char *editTail(void) {
+char *editTail(void) {
 	static char mbs[MB_LEN_MAX * Cap];
 	const wchar_t *ptr = &buf[pos];
-	size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs), NULL);
+	size_t n = wcsnrtombs(mbs, &ptr, len - pos, sizeof(mbs) - 1, NULL);
 	assert(n != (size_t)-1);
+	mbs[n] = '\0';
 	return mbs;
 }
+
+void edit(size_t id, enum Edit op, wchar_t ch) {
+	switch (op) {
+		break; case EditKill: len = pos = 0;
+		break; case EditInsert: {
+			if (len == Cap) break;
+			buf[pos++] = ch;
+			len++;
+		}
+		break; case EditEnter: {
+			pos = 0;
+			command(id, editTail());
+			len = 0;
+		}
+	}
+}
diff --git a/ui.c b/ui.c
index 8f813b4..b764f84 100644
--- a/ui.c
+++ b/ui.c
@@ -498,8 +498,11 @@ static void keyMeta(wchar_t ch) {
 }
 
 static void keyCtrl(wchar_t ch) {
+	size_t id = windows.active->id;
 	switch (ch) {
+		break; case L'J': edit(id, EditEnter, 0);
 		break; case L'L': clearok(curscr, true);
+		break; case L'U': edit(id, EditKill, 0);
 	}
 }
 
@@ -518,7 +521,7 @@ void uiRead(void) {
 		} else if (iswcntrl(ch)) {
 			keyCtrl(ch ^ L'@');
 		} else {
-			// TODO: Insert.
+			edit(windows.active->id, EditInsert, ch);
 		}
 		meta = false;
 	}
-- 
cgit 1.4.1-2-gfad0


From 27eaddb6b9524b43448f4e5c88ac74bbe8fdb3a5 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Wed, 5 Feb 2020 22:49:56 -0500
Subject: Use getopt_config to load options

I'm really getting a lot of use out of this config.c huh.
---
 Makefile  |   1 +
 catgirl.1 |  41 ++++++++++-----
 chat.c    |  20 ++++++-
 chat.h    |   7 +++
 config.c  | 178 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 234 insertions(+), 13 deletions(-)
 create mode 100644 config.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 05f8bb8..213ecb5 100644
--- a/Makefile
+++ b/Makefile
@@ -7,6 +7,7 @@ LDLIBS = -lcurses -lcrypto -ltls
 
 OBJS += chat.o
 OBJS += command.o
+OBJS += config.o
 OBJS += edit.o
 OBJS += handle.o
 OBJS += irc.o
diff --git a/catgirl.1 b/catgirl.1
index fb031c2..91c4ff4 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -19,6 +19,7 @@
 .Op Fl r Ar real
 .Op Fl u Ar user
 .Op Fl w Ar pass
+.Op Ar config ...
 .
 .Sh DESCRIPTION
 The
@@ -27,9 +28,25 @@ program is a curses
 TLS-only IRC client.
 .
 .Pp
+Options can be loaded from files
+listed on the command line.
+Files are searched for in
+.Pa $XDG_CONFIG_DIRS/catgirl
+unless the path starts with
+.Ql /
+or
+.Ql \&. .
+Each option is placed on a line,
+and lines beginning with
+.Ql #
+are ignored.
+The options are listed below
+following their corresponding flags.
+.
+.Pp
 The arguments are as follows:
 .Bl -tag -width Ds
-.It Fl a Ar user Ns : Ns Ar pass
+.It Fl a Ar user Ns : Ns Ar pass , Cm sasl-plain = Ar user Ns : Ns Ar pass
 Authenticate as
 .Ar user
 with
@@ -40,7 +57,7 @@ in plain text,
 it is recommended to use SASL EXTERNAL instead with
 .Fl e .
 .
-.It Fl c Ar path
+.It Fl c Ar path , Cm cert = Ar path
 Load the TLS client certificate from
 .Ar path .
 If the private key is in a separate file,
@@ -50,52 +67,52 @@ With
 .Fl e ,
 authenticate using SASL EXTERNAL.
 .
-.It Fl e
+.It Fl e , Cm sasl-external
 Authenticate using SASL EXTERNAL,
 also known as CertFP.
 The TLS client certificate is loaded with
 .Fl c .
 .
-.It Fl h Ar host
+.It Fl h Ar host , Cm host = Ar host
 Connect to
 .Ar host .
 .
-.It Fl j Ar join
+.It Fl j Ar join , Cm join = Ar join
 Join the comma-separated list of channels
 .Ar join .
 .
-.It Fl k Ar path
+.It Fl k Ar path , Cm priv = Ar priv
 Load the TLS client private key from
 .Ar path .
 .
-.It Fl n Ar nick
+.It Fl n Ar nick , Cm nick = Ar nick
 Set nickname to
 .Ar nick .
 The default nickname is the user's name.
 .
-.It Fl p Ar port
+.It Fl p Ar port , Cm port = Ar port
 Connect to
 .Ar port .
 The default port is 6697.
 .
-.It Fl r Ar real
+.It Fl r Ar real , Cm real = Ar real
 Set realname to
 .Ar real .
 The default realname is the same as the nickname.
 .
-.It Fl u Ar user
+.It Fl u Ar user , Cm user = Ar user
 Set username to
 .Ar user .
 The default username is the same as the nickname.
 .
-.It Fl v
+.It Fl v , Cm debug
 Log raw IRC messages to the
 .Sy <debug>
 window
 as well as standard error
 if it is not a terminal.
 .
-.It Fl w Ar pass
+.It Fl w Ar pass , Cm pass = Ar pass
 Log in with the server password
 .Ar pass .
 .El
diff --git a/chat.c b/chat.c
index 1ad2833..c67d8a9 100644
--- a/chat.c
+++ b/chat.c
@@ -63,8 +63,26 @@ int main(int argc, char *argv[]) {
 	const char *user = NULL;
 	const char *real = NULL;
 
+	const char *Opts = "!a:c:eh:j:k:n:p:r:u:vw:";
+	const struct option LongOpts[] = {
+		{ "insecure", no_argument, NULL, '!' },
+		{ "sasl-plain", required_argument, NULL, 'a' },
+		{ "cert", required_argument, NULL, 'c' },
+		{ "sasl-external", no_argument, NULL, 'e' },
+		{ "host", required_argument, NULL, 'h' },
+		{ "join", required_argument, NULL, 'j' },
+		{ "priv", required_argument, NULL, 'k' },
+		{ "nick", required_argument, NULL, 'n' },
+		{ "port", required_argument, NULL, 'p' },
+		{ "real", required_argument, NULL, 'r' },
+		{ "user", required_argument, NULL, 'u' },
+		{ "debug", no_argument, NULL, 'v' },
+		{ "pass", required_argument, NULL, 'w' },
+		{0},
+	};
+
 	int opt;
-	while (0 < (opt = getopt(argc, argv, "!a:c:eh:j:k:n:p:r:u:vw:"))) {
+	while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) {
 		switch (opt) {
 			break; case '!': insecure = true;
 			break; case 'a': sasl = true; self.plain = optarg;
diff --git a/chat.h b/chat.h
index 9317843..57d4ba6 100644
--- a/chat.h
+++ b/chat.h
@@ -15,6 +15,7 @@
  */
 
 #include <err.h>
+#include <getopt.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <string.h>
@@ -139,6 +140,12 @@ void edit(size_t id, enum Edit op, wchar_t ch);
 char *editHead(void);
 char *editTail(void);
 
+FILE *configOpen(const char *path, const char *mode);
+int getopt_config(
+	int argc, char *const *argv,
+	const char *optstring, const struct option *longopts, int *longindex
+);
+
 static inline enum Color hash(const char *str) {
 	if (*str == '~') str++;
 	uint32_t hash = 0;
diff --git a/config.c b/config.c
new file mode 100644
index 0000000..b3e42f9
--- /dev/null
+++ b/config.c
@@ -0,0 +1,178 @@
+/* Copyright (C) 2019  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <errno.h>
+#include <getopt.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "chat.h"
+
+#define CONFIG_DIR "catgirl"
+
+FILE *configOpen(const char *path, const char *mode) {
+	if (path[0] == '/' || path[0] == '.') goto local;
+
+	const char *home = getenv("HOME");
+	const char *configHome = getenv("XDG_CONFIG_HOME");
+	const char *configDirs = getenv("XDG_CONFIG_DIRS");
+
+	char buf[PATH_MAX];
+	if (configHome) {
+		snprintf(buf, sizeof(buf), "%s/" CONFIG_DIR "/%s", configHome, path);
+	} else {
+		if (!home) goto local;
+		snprintf(buf, sizeof(buf), "%s/.config/" CONFIG_DIR "/%s", home, path);
+	}
+	FILE *file = fopen(buf, mode);
+	if (file) return file;
+	if (errno != ENOENT) return NULL;
+
+	if (!configDirs) configDirs = "/etc/xdg";
+	while (*configDirs) {
+		size_t len = strcspn(configDirs, ":");
+		snprintf(
+			buf, sizeof(buf), "%.*s/" CONFIG_DIR "/%s",
+			(int)len, configDirs, path
+		);
+		file = fopen(buf, mode);
+		if (file) return file;
+		if (errno != ENOENT) return NULL;
+		configDirs += len;
+		if (*configDirs) configDirs++;
+	}
+
+local:
+	return fopen(path, mode);
+}
+
+#define WS "\t "
+
+static const char *path;
+static FILE *file;
+static size_t num;
+static char *line;
+static size_t cap;
+
+static int clean(int opt) {
+	if (file) fclose(file);
+	free(line);
+	line = NULL;
+	cap = 0;
+	return opt;
+}
+
+int getopt_config(
+	int argc, char *const *argv,
+	const char *optstring, const struct option *longopts, int *longindex
+) {
+	static int opt;
+	if (opt >= 0) {
+		opt = getopt_long(argc, argv, optstring, longopts, longindex);
+	}
+	if (opt >= 0) return opt;
+
+	for (;;) {
+		if (!file) {
+			if (optind < argc) {
+				num = 0;
+				path = argv[optind++];
+				file = configOpen(path, "r");
+				if (!file) {
+					warn("%s", path);
+					return clean('?');
+				}
+			} else {
+				return clean(-1);
+			}
+		}
+
+		for (;;) {
+			ssize_t llen = getline(&line, &cap, file);
+			if (ferror(file)) {
+				warn("%s", path);
+				return clean('?');
+			}
+			if (llen <= 0) break;
+			if (line[llen - 1] == '\n') line[llen - 1] = '\0';
+			num++;
+
+			char *name = line + strspn(line, WS);
+			size_t len = strcspn(name, WS "=");
+			if (!name[0] || name[0] == '#') continue;
+
+			const struct option *option;
+			for (option = longopts; option->name; ++option) {
+				if (strlen(option->name) != len) continue;
+				if (!strncmp(option->name, name, len)) break;
+			}
+			if (!option->name) {
+				warnx(
+					"%s:%zu: unrecognized option `%.*s'",
+					path, num, (int)len, name
+				);
+				return clean('?');
+			}
+
+			char *equal = &name[len] + strspn(&name[len], WS);
+			if (*equal && *equal != '=') {
+				warnx(
+					"%s:%zu: option `%s' missing equals sign",
+					path, num, option->name
+				);
+				return clean('?');
+			}
+			if (option->has_arg == no_argument && *equal) {
+				warnx(
+					"%s:%zu: option `%s' doesn't allow an argument",
+					path, num, option->name
+				);
+				return clean('?');
+			}
+			if (option->has_arg == required_argument && !*equal) {
+				warnx(
+					"%s:%zu: option `%s' requires an argument",
+					path, num, option->name
+				);
+				return clean(':');
+			}
+
+			optarg = NULL;
+			if (*equal) {
+				char *arg = &equal[1] + strspn(&equal[1], WS);
+				optarg = strdup(arg);
+				if (!optarg) {
+					warn("getopt_config");
+					return clean('?');
+				}
+			}
+
+			if (longindex) *longindex = option - longopts;
+			if (option->flag) {
+				*option->flag = option->val;
+				return 0;
+			} else {
+				return option->val;
+			}
+		}
+
+		fclose(file);
+		file = NULL;
+	}
+}
-- 
cgit 1.4.1-2-gfad0


From 5470254fa5fd0f6108b1f075d9ac2dd24afa7fdc Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Thu, 6 Feb 2020 23:49:27 -0500
Subject: Add simple configure script

Mostly motivated by wanting to build with the ncurses in pkgsrc because
it supports italics.
---
 .gitignore |  1 +
 Makefile   |  8 +++-----
 configure  | 11 +++++++++++
 3 files changed, 15 insertions(+), 5 deletions(-)
 create mode 100755 configure

(limited to 'Makefile')

diff --git a/.gitignore b/.gitignore
index 64b2b13..4cc4220 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 *.o
 catgirl
+config.mk
 tags
diff --git a/Makefile b/Makefile
index 213ecb5..5380d20 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,7 @@
-LIBRESSL_PREFIX = /usr/local
-CFLAGS += -I${LIBRESSL_PREFIX}/include
-LDFLAGS += -L${LIBRESSL_PREFIX}/lib
-
 CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
-LDLIBS = -lcurses -lcrypto -ltls
+LDLIBS = -lcrypto -ltls -lncursesw
+
+-include config.mk
 
 OBJS += chat.o
 OBJS += command.o
diff --git a/configure b/configure
new file mode 100755
index 0000000..90e1173
--- /dev/null
+++ b/configure
@@ -0,0 +1,11 @@
+#!/bin/sh
+set -eu
+
+libs='libcrypto libtls ncursesw'
+pkg-config --print-errors $libs
+
+cat >config.mk <<EOF
+CFLAGS += $(pkg-config --cflags $libs)
+LDFLAGS += $(pkg-config --libs-only-L $libs)
+LDLIBS = $(pkg-config --libs-only-l $libs)
+EOF
-- 
cgit 1.4.1-2-gfad0


From fe5fd897052abd1909d1536056936a0417666459 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Fri, 7 Feb 2020 21:30:25 -0500
Subject: Populate completion with commands

---
 Makefile   |   1 +
 chat.c     |   1 +
 chat.h     |   8 +++++
 command.c  |   6 ++++
 complete.c | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 edit.c     |   3 ++
 ui.c       |   1 +
 7 files changed, 130 insertions(+)
 create mode 100644 complete.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 5380d20..48aba7b 100644
--- a/Makefile
+++ b/Makefile
@@ -5,6 +5,7 @@ LDLIBS = -lcrypto -ltls -lncursesw
 
 OBJS += chat.o
 OBJS += command.o
+OBJS += complete.o
 OBJS += config.o
 OBJS += edit.o
 OBJS += handle.o
diff --git a/chat.c b/chat.c
index c487722..91da6e3 100644
--- a/chat.c
+++ b/chat.c
@@ -110,6 +110,7 @@ int main(int argc, char *argv[]) {
 	set(&self.network, host);
 	set(&self.chanTypes, "#&");
 	set(&self.prefixes, "@+");
+	commandComplete();
 
 	FILE *certFile = NULL;
 	FILE *privFile = NULL;
diff --git a/chat.h b/chat.h
index a327620..f164e7a 100644
--- a/chat.h
+++ b/chat.h
@@ -118,6 +118,7 @@ void command(size_t id, char *input);
 const char *commandIsPrivmsg(size_t id, const char *input);
 const char *commandIsNotice(size_t id, const char *input);
 const char *commandIsAction(size_t id, const char *input);
+void commandComplete(void);
 
 enum Heat { Cold, Warm, Hot };
 void uiInit(void);
@@ -140,12 +141,19 @@ enum Edit {
 	EditKill,
 	EditErase,
 	EditInsert,
+	EditComplete,
 	EditEnter,
 };
 void edit(size_t id, enum Edit op, wchar_t ch);
 char *editHead(void);
 char *editTail(void);
 
+const char *complete(size_t id, const char *prefix);
+void completeAccept(void);
+void completeReject(void);
+void completeAdd(size_t id, const char *str, enum Color color);
+void completeTouch(size_t id, const char *str, enum Color color);
+
 FILE *configOpen(const char *path, const char *mode);
 int getopt_config(
 	int argc, char *const *argv,
diff --git a/command.c b/command.c
index 3215322..41aacc9 100644
--- a/command.c
+++ b/command.c
@@ -136,3 +136,9 @@ void command(size_t id, char *input) {
 		}
 	}
 }
+
+void commandComplete(void) {
+	for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) {
+		completeAdd(None, Commands[i].cmd, Default);
+	}
+}
diff --git a/complete.c b/complete.c
new file mode 100644
index 0000000..b8f2dfc
--- /dev/null
+++ b/complete.c
@@ -0,0 +1,110 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+
+#include "chat.h"
+
+struct Node {
+	size_t id;
+	char *str;
+	enum Color color;
+	struct Node *prev;
+	struct Node *next;
+};
+
+static struct Node *alloc(size_t id, const char *str, enum Color color) {
+	struct Node *node = malloc(sizeof(*node));
+	if (!node) err(EX_OSERR, "malloc");
+	node->id = id;
+	node->str = strdup(str);
+	if (!node->str) err(EX_OSERR, "strdup");
+	node->color = color;
+	node->prev = NULL;
+	node->next = NULL;
+	return node;
+}
+
+static struct Node *head;
+static struct Node *tail;
+
+static struct Node *detach(struct Node *node) {
+	if (node->prev) node->prev->next = node->next;
+	if (node->next) node->next->prev = node->prev;
+	if (head == node) head = node->next;
+	if (tail == node) tail = node->prev;
+	node->prev = NULL;
+	node->next = NULL;
+	return node;
+}
+
+static struct Node *prepend(struct Node *node) {
+	node->prev = NULL;
+	node->next = head;
+	if (head) head->prev = node;
+	head = node;
+	if (!tail) tail = node;
+	return node;
+}
+
+static struct Node *append(struct Node *node) {
+	node->next = NULL;
+	node->prev = tail;
+	if (tail) tail->next = node;
+	tail = node;
+	if (!head) head = node;
+	return node;
+}
+
+static struct Node *find(size_t id, const char *str) {
+	for (struct Node *node = head; node; node = node->next) {
+		if (node->id == id && !strcmp(node->str, str)) return node;
+	}
+	return NULL;
+}
+
+void completeAdd(size_t id, const char *str, enum Color color) {
+	if (!find(id, str)) append(alloc(id, str, color));
+}
+
+void completeTouch(size_t id, const char *str, enum Color color) {
+	struct Node *node = find(id, str);
+	prepend(node ? detach(node) : alloc(id, str, color));
+}
+
+static struct Node *match;
+
+const char *complete(size_t id, const char *prefix) {
+	for (match = (match ? match->next : head); match; match = match->next) {
+		if (match->id && match->id != id) continue;
+		if (strncasecmp(match->str, prefix, strlen(prefix))) continue;
+		return match->str;
+	}
+	return NULL;
+}
+
+void completeAccept(void) {
+	if (match) prepend(detach(match));
+	match = NULL;
+}
+
+void completeReject(void) {
+	match = NULL;
+}
diff --git a/edit.c b/edit.c
index b6edb98..0c50f33 100644
--- a/edit.c
+++ b/edit.c
@@ -73,6 +73,9 @@ void edit(size_t id, enum Edit op, wchar_t ch) {
 			reserve(pos, 1);
 			if (pos < Cap) buf[pos++] = ch;
 		}
+		break; case EditComplete: {
+			// TODO
+		}
 		break; case EditEnter: {
 			pos = 0;
 			command(id, editTail());
diff --git a/ui.c b/ui.c
index 147381e..5a8f155 100644
--- a/ui.c
+++ b/ui.c
@@ -596,6 +596,7 @@ static void keyCtrl(wchar_t ch) {
 		break; case L'A': edit(id, EditHome, 0);
 		break; case L'E': edit(id, EditEnd, 0);
 		break; case L'H': edit(id, EditErase, 0);
+		break; case L'I': edit(id, EditComplete, 0);
 		break; case L'J': edit(id, EditEnter, 0);
 		break; case L'L': clearok(curscr, true);
 		break; case L'U': edit(id, EditKill, 0);
-- 
cgit 1.4.1-2-gfad0


From f502260dd0aa73b09bfbb7289b50a67592866166 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sat, 8 Feb 2020 18:29:01 -0500
Subject: Scan messages for URLs

---
 Makefile  |  1 +
 catgirl.1 |  9 ++++++
 chat.h    |  4 +++
 command.c | 11 ++++++++
 handle.c  | 15 ++++++++--
 url.c     | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 133 insertions(+), 3 deletions(-)
 create mode 100644 url.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 48aba7b..bcbb0d8 100644
--- a/Makefile
+++ b/Makefile
@@ -11,6 +11,7 @@ OBJS += edit.o
 OBJS += handle.o
 OBJS += irc.o
 OBJS += ui.o
+OBJS += url.o
 
 dev: tags all
 
diff --git a/catgirl.1 b/catgirl.1
index 5394d33..f489d07 100644
--- a/catgirl.1
+++ b/catgirl.1
@@ -156,6 +156,15 @@ Close the named, numbered or current window.
 Toggle logging in the
 .Sy <debug>
 window.
+.It Ic /open Op Ar count
+Open each of
+.Ar count
+most recent URLs.
+.It Ic /open Ar nick | substring
+Open the most recent URL from
+.Ar nick
+or matching
+.Ar substring .
 .It Ic /window Ar name
 Switch to window by name.
 .It Ic /window Ar num , Ic / Ns Ar num
diff --git a/chat.h b/chat.h
index 909527e..583107a 100644
--- a/chat.h
+++ b/chat.h
@@ -169,6 +169,10 @@ void completeClear(size_t id);
 size_t completeID(const char *str);
 enum Color completeColor(size_t id, const char *str);
 
+void urlScan(size_t id, const char *nick, const char *mesg);
+void urlOpenCount(size_t id, size_t count);
+void urlOpenMatch(size_t id, const char *str);
+
 FILE *configOpen(const char *path, const char *mode);
 int getopt_config(
 	int argc, char *const *argv,
diff --git a/command.c b/command.c
index eaabc9c..4100928 100644
--- a/command.c
+++ b/command.c
@@ -144,6 +144,16 @@ static void commandClose(size_t id, char *params) {
 	}
 }
 
+static void commandOpen(size_t id, char *params) {
+	if (!params) {
+		urlOpenCount(id, 1);
+	} else if (isdigit(params[0])) {
+		urlOpenCount(id, strtoul(params, NULL, 10));
+	} else {
+		urlOpenMatch(id, params);
+	}
+}
+
 static const struct Handler {
 	const char *cmd;
 	Command *fn;
@@ -155,6 +165,7 @@ static const struct Handler {
 	{ "/names", commandNames },
 	{ "/nick", commandNick },
 	{ "/notice", commandNotice },
+	{ "/open", commandOpen },
 	{ "/part", commandPart },
 	{ "/query", commandQuery },
 	{ "/quit", commandQuit },
diff --git a/handle.c b/handle.c
index 0780767..f919fcb 100644
--- a/handle.c
+++ b/handle.c
@@ -193,6 +193,7 @@ static void handleReplyISupport(struct Message *msg) {
 static void handleReplyMOTD(struct Message *msg) {
 	require(msg, false, 2);
 	char *line = msg->params[1];
+	urlScan(Network, msg->nick, line);
 	if (!strncmp(line, "- ", 2)) {
 		uiFormat(Network, Cold, tagTime(msg), "\3%d-\3\t%s", Gray, &line[2]);
 	} else {
@@ -227,6 +228,7 @@ static void handlePart(struct Message *msg) {
 		completeClear(id);
 	}
 	completeRemove(id, msg->nick);
+	urlScan(id, msg->nick, msg->params[1]);
 	uiFormat(
 		id, Cold, tagTime(msg),
 		"\3%02d%s\3\tleaves \3%02d%s\3%s%s",
@@ -241,6 +243,7 @@ static void handleKick(struct Message *msg) {
 	size_t id = idFor(msg->params[0]);
 	bool kicked = self.nick && !strcmp(msg->params[1], self.nick);
 	completeTouch(id, msg->nick, hash(msg->user));
+	urlScan(id, msg->nick, msg->params[2]);
 	uiFormat(
 		id, (kicked ? Hot : Cold), tagTime(msg),
 		"%s\3%02d%s\17\tkicks \3%02d%s\3 out of \3%02d%s\3%s%s",
@@ -275,6 +278,7 @@ static void handleQuit(struct Message *msg) {
 	require(msg, true, 0);
 	size_t id;
 	while (None != (id = completeID(msg->nick))) {
+		urlScan(id, msg->nick, msg->params[0]);
 		uiFormat(
 			id, Cold, tagTime(msg),
 			"\3%02d%s\3\tleaves%s%s",
@@ -333,8 +337,10 @@ static void handleReplyTopic(struct Message *msg) {
 	require(msg, false, 3);
 	if (!replies.topic) return;
 	replies.topic--;
+	size_t id = idFor(msg->params[1]);
+	urlScan(id, NULL, msg->params[2]);
 	uiFormat(
-		idFor(msg->params[1]), Cold, tagTime(msg),
+		id, Cold, tagTime(msg),
 		"The sign in \3%02d%s\3 reads: %s",
 		hash(msg->params[1]), msg->params[1], msg->params[2]
 	);
@@ -342,16 +348,18 @@ static void handleReplyTopic(struct Message *msg) {
 
 static void handleTopic(struct Message *msg) {
 	require(msg, true, 2);
+	size_t id = idFor(msg->params[0]);
 	if (msg->params[1][0]) {
+		urlScan(id, msg->nick, msg->params[1]);
 		uiFormat(
-			idFor(msg->params[0]), Warm, tagTime(msg),
+			id, Warm, tagTime(msg),
 			"\3%02d%s\3\tplaces a new sign in \3%02d%s\3: %s",
 			hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0],
 			msg->params[1]
 		);
 	} else {
 		uiFormat(
-			idFor(msg->params[0]), Warm, tagTime(msg),
+			id, Warm, tagTime(msg),
 			"\3%02d%s\3\tremoves the sign in \3%02d%s\3",
 			hash(msg->user), msg->nick, hash(msg->params[0]), msg->params[0]
 		);
@@ -400,6 +408,7 @@ static void handlePrivmsg(struct Message *msg) {
 	bool action = isAction(msg);
 	bool mention = !mine && isMention(msg);
 	if (!notice && !mine) completeTouch(id, msg->nick, hash(msg->user));
+	urlScan(id, msg->nick, msg->params[1]);
 	if (notice) {
 		uiFormat(
 			id, Warm, tagTime(msg),
diff --git a/url.c b/url.c
new file mode 100644
index 0000000..7790461
--- /dev/null
+++ b/url.c
@@ -0,0 +1,96 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <regex.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysexits.h>
+
+#include "chat.h"
+
+static const char *Pattern = {
+	"("
+	"cvs|"
+	"ftp|"
+	"git|"
+	"gopher|"
+	"http|"
+	"https|"
+	"irc|"
+	"ircs|"
+	"magnet|"
+	"sftp|"
+	"ssh|"
+	"svn|"
+	"telnet|"
+	"vnc"
+	")"
+	":[^[:space:]>\"]+"
+};
+static regex_t Regex;
+
+static void compile(void) {
+	static bool compiled;
+	if (compiled) return;
+	compiled = true;
+	int error = regcomp(&Regex, Pattern, REG_EXTENDED);
+	if (!error) return;
+	char buf[256];
+	regerror(error, &Regex, buf, sizeof(buf));
+	errx(EX_SOFTWARE, "regcomp: %s: %s", buf, Pattern);
+}
+
+enum { Cap = 32 };
+static struct {
+	size_t ids[Cap];
+	char *nicks[Cap];
+	char *urls[Cap];
+	size_t len;
+} ring;
+
+static void push(size_t id, const char *nick, const char *url, size_t len) {
+	size_t i = ring.len++ % Cap;
+	free(ring.nicks[i]);
+	free(ring.urls[i]);
+	ring.ids[i] = id;
+	ring.nicks[i] = NULL;
+	if (nick) {
+		ring.nicks[i] = strdup(nick);
+		if (!ring.nicks[i]) err(EX_OSERR, "strdup");
+	}
+	ring.urls[i] = strndup(url, len);
+	if (!ring.urls[i]) err(EX_OSERR, "strndup");
+}
+
+void urlScan(size_t id, const char *nick, const char *mesg) {
+	if (!mesg) return;
+	compile();
+	regmatch_t match = {0};
+	for (const char *ptr = mesg; *ptr; ptr += match.rm_eo) {
+		if (regexec(&Regex, ptr, 1, &match, 0)) break;
+		push(id, nick, &ptr[match.rm_so], match.rm_eo - match.rm_so);
+	}
+}
+
+void urlOpenCount(size_t id, size_t count) {
+	// TODO
+}
+
+void urlOpenMatch(size_t id, const char *str) {
+	// TODO
+}
-- 
cgit 1.4.1-2-gfad0


From 1d26c880ed305951715a548f934eb231b8fc9317 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Sun, 9 Feb 2020 15:02:47 -0500
Subject: Add install target

---
 Makefile | 11 +++++++++++
 1 file changed, 11 insertions(+)

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index bcbb0d8..89af9b3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,6 @@
+PREFIX = /usr/local
+MANDIR = ${PREFIX}/share/man
+
 CFLAGS += -std=c11 -Wall -Wextra -Wpedantic
 LDLIBS = -lcrypto -ltls -lncursesw
 
@@ -27,3 +30,11 @@ tags: *.h *.c
 
 clean:
 	rm -f tags catgirl ${OBJS}
+
+install: catgirl catgirl.1
+	install -d ${PREFIX}/bin ${MANDIR}/man1
+	install catgirl ${PREFIX}/bin
+	gzip -c catgirl.1 > ${MANDIR}/man1/catgirl.1.gz
+
+uninstall:
+	rm -f ${PREFIX}/bin/catgirl ${MANDIR}/man1/catgirl.1.gz
-- 
cgit 1.4.1-2-gfad0


From 99480a42e56e70707822934ffeb56f0454afc127 Mon Sep 17 00:00:00 2001
From: C. McEnroe
Date: Mon, 10 Feb 2020 19:57:10 -0500
Subject: Factor out XDG base directory code

And add warnings to configOpen, since that's the only way to be accurate
if a weird error occurs.
---
 Makefile |   1 +
 chat.c   |   4 +-
 chat.h   |   2 +
 config.c |  41 +------------------
 ui.c     |  67 --------------------------------
 xdg.c    | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 6 files changed, 140 insertions(+), 109 deletions(-)
 create mode 100644 xdg.c

(limited to 'Makefile')

diff --git a/Makefile b/Makefile
index 89af9b3..b1ffede 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,7 @@ OBJS += handle.o
 OBJS += irc.o
 OBJS += ui.o
 OBJS += url.o
+OBJS += xdg.o
 
 dev: tags all
 
diff --git a/chat.c b/chat.c
index e8713bb..1ae7090 100644
--- a/chat.c
+++ b/chat.c
@@ -154,11 +154,11 @@ int main(int argc, char *argv[]) {
 	FILE *privFile = NULL;
 	if (cert) {
 		certFile = configOpen(cert, "r");
-		if (!certFile) err(EX_NOINPUT, "%s", cert);
+		if (!certFile) return EX_NOINPUT;
 	}
 	if (priv) {
 		privFile = configOpen(priv, "r");
-		if (!privFile) err(EX_NOINPUT, "%s", priv);
+		if (!privFile) return EX_NOINPUT;
 	}
 	ircConfig(insecure, certFile, privFile);
 	if (certFile) fclose(certFile);
diff --git a/chat.h b/chat.h
index 47a6163..03a0a50 100644
--- a/chat.h
+++ b/chat.h
@@ -189,6 +189,8 @@ void urlOpenMatch(size_t id, const char *str);
 void urlCopyMatch(size_t id, const char *str);
 
 FILE *configOpen(const char *path, const char *mode);
+FILE *dataOpen(const char *path, const char *mode);
+
 int getopt_config(
 	int argc, char *const *argv,
 	const char *optstring, const struct option *longopts, int *longindex
diff --git a/config.c b/config.c
index 3bf56c0..3a87948 100644
--- a/config.c
+++ b/config.c
@@ -24,42 +24,6 @@
 
 #include "chat.h"
 
-FILE *configOpen(const char *path, const char *mode) {
-	if (path[0] == '/' || path[0] == '.') goto local;
-
-	const char *home = getenv("HOME");
-	const char *configHome = getenv("XDG_CONFIG_HOME");
-	const char *configDirs = getenv("XDG_CONFIG_DIRS");
-
-	char buf[PATH_MAX];
-	if (configHome) {
-		snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path);
-	} else {
-		if (!home) goto local;
-		snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path);
-	}
-	FILE *file = fopen(buf, mode);
-	if (file) return file;
-	if (errno != ENOENT) return NULL;
-
-	if (!configDirs) configDirs = "/etc/xdg";
-	while (*configDirs) {
-		size_t len = strcspn(configDirs, ":");
-		snprintf(
-			buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s",
-			(int)len, configDirs, path
-		);
-		file = fopen(buf, mode);
-		if (file) return file;
-		if (errno != ENOENT) return NULL;
-		configDirs += len;
-		if (*configDirs) configDirs++;
-	}
-
-local:
-	return fopen(path, mode);
-}
-
 #define WS "\t "
 
 static const char *path;
@@ -92,10 +56,7 @@ int getopt_config(
 				num = 0;
 				path = argv[optind++];
 				file = configOpen(path, "r");
-				if (!file) {
-					warn("%s", path);
-					return clean('?');
-				}
+				if (!file) return clean('?');
 			} else {
 				return clean(-1);
 			}
diff --git a/ui.c b/ui.c
index ecf7e60..9601aaa 100644
--- a/ui.c
+++ b/ui.c
@@ -21,13 +21,11 @@
 #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>
@@ -858,71 +856,6 @@ 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,
 };
diff --git a/xdg.c b/xdg.c
new file mode 100644
index 0000000..6e33210
--- /dev/null
+++ b/xdg.c
@@ -0,0 +1,134 @@
+/* Copyright (C) 2019, 2020  C. McEnroe <june@causal.agency>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "chat.h"
+
+FILE *configOpen(const char *path, const char *mode) {
+	if (path[0] == '/' || path[0] == '.') goto local;
+
+	const char *home = getenv("HOME");
+	const char *configHome = getenv("XDG_CONFIG_HOME");
+	const char *configDirs = getenv("XDG_CONFIG_DIRS");
+
+	char buf[PATH_MAX];
+	if (configHome) {
+		snprintf(buf, sizeof(buf), "%s/" XDG_SUBDIR "/%s", configHome, path);
+	} else {
+		if (!home) goto local;
+		snprintf(buf, sizeof(buf), "%s/.config/" XDG_SUBDIR "/%s", home, path);
+	}
+	FILE *file = fopen(buf, mode);
+	if (file) return file;
+	if (errno != ENOENT) {
+		warn("%s", buf);
+		return NULL;
+	}
+
+	if (!configDirs) configDirs = "/etc/xdg";
+	while (*configDirs) {
+		size_t len = strcspn(configDirs, ":");
+		snprintf(
+			buf, sizeof(buf), "%.*s/" XDG_SUBDIR "/%s",
+			(int)len, configDirs, path
+		);
+		file = fopen(buf, mode);
+		if (file) return file;
+		if (errno != ENOENT) {
+			warn("%s", buf);
+			return NULL;
+		}
+		configDirs += len;
+		if (*configDirs) configDirs++;
+	}
+
+local:
+	file = fopen(path, mode);
+	if (!file) warn("%s", path);
+	return file;
+}
+
+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;
+}
-- 
cgit 1.4.1-2-gfad0