about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Makefile1
-rw-r--r--catgirl.141
-rw-r--r--chat.c20
-rw-r--r--chat.h7
-rw-r--r--config.c178
5 files changed, 234 insertions, 13 deletions
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;
+	}
+}